Statements vs. Expressions
joshwcomeau.com
joshwcomeau.com
This isn't going to be explained satisfactorily by analogies or intuition, and it won't be logical at all, because it isn't, and you could define the grammar with no such distinction or a different set of distinctions as many posters already showed here.
Besides being arbitrary, I also never found this a useful distinction after having programmed in languages that don't distinguish this. And look at how GCC introduced ({...}) to convert back a statement into an expression.
Anyway, to define this, you have to simply list all the things: statements are 'return', '{...}', 'while() {... }', etc., while expressions are '5', 'f(...)', etc. That's it, that's the explanation. No intuition, just a comprehensive list taken from the grammar.
If course there is some rough idea behind it, which explains why there is a difference, but this is a history class, not an explanation: statements do some action, i.e., this is an imperative concept. Expressions have a value, i.e., this is a functional concept.
If the question is how they are different, then the answer is grammatically, and then the explanation of a grammar follows.
If it is why we make such distinction, then the answer is because the language creator decided so, followed by tradeoffs and pitfalls of making a specific language construct to have a value.
I also don't think he does a good job at explaining the biggest source of misunderstanding. Quoting the article:
`Expressions and statements are distinct things.`
That is not really correct.
A function call, e.g. is both an expression and a statement:
`foo(bar)` on it's own is both an expression (it returns a value, even if the value returned is undefined) and a statement.
In fact, determining if a piece of code is an expression or a statement in all borderline cases depends on the context.
```
const foo = function bar(){};
function bar(){}
```
On line 1 the bar function is an expression, on bar 3, is a statement.
There's nothing really about the function on it's own that can tell you whether it is an expression or a statement.
On the other hand, a nice way to differentiate statements from expressions is that you cannot use statements where you need expressions.
```
console.log(if (3 > 2) {})
const foo = while (i < 3) {} ```
both lines are syntax errors.
expression
- generally returns a value, possibly a void value, or doesn't return control at all.
- it consists out of operators, operands, and function calls
- can be composed out of one or more sub-expressions, and the order of their evaluation isn't always defined (i.e. to allow compiler optimizations).
- usually it doesn't have side-effects (i.e. pure), unless it uses random, time, IPC, system, or HW-specific pseudo-functions, pseudo-variables, or macros as a sub-expressions. Nested assignments and variable increments/decrements/etc. are also not pure.
- a result of a function call
statement
- usually an imperative command
- usually indicate a side-effect (e.g. assignment, sending a message, etc.)
- can be a procedure call or macro
- can be composed out of expressions and nested statements, but with better defined semantics / order of evaluation
- a special case: control flow statements
- a special case: "return" statement - evaluate a value and returns it as a result of the current function
> - a special case: control flow statements
But it’s worth noting that this is not true in all languages: in some languages, almost everything or actually everything is an expression. In Rust, for example, control flow things may be statements or expressions (a mildly fuzzy distinction, but it comes down to whether it produces a value other than (), and whether it needs a semicolon after the closing curly brace), and some control flow things are very useful as expressions:
let a = if b {
c
} else {
d
};
let e = loop {
break f; // f is a value, not a label (which uses lifetime syntax, 'g).
};In some cases it can be optimized to remove branching, especially when vectorized for SIMD/SIMT.
int a,b,c,d;
a = b ? c : d;
// optimized to something like this:
a = b*c + (1-b)*d
In some PLs even loop statements can produce values (CoffeeScript, Autocode), i.e. similar to list comprehensions, which are undoubtedly expressions.I'm a kdb+/q developer, and most of the q code are expressions without conventional control flow operators (the only place they're used is outside of the main path: i.e. logging, testing, etc.).
IMO control flow (i.e. actual branching and looping) is just an implementation detail.
That’s six expressions, not five. Josh conflates the the addition expression and the parenthesised expression, but they’re distinct (even if you couldn’t represent the semantics without the parenthesis due to operator precedence).
Here’s an approximate representation of the parse tree, indicating which nodes are expressions:
MultiplicativeExpression { ←
ParenthesizedExpression { ←
AdditiveExpression { ←
DecimalIntegerLiteral("5") ←
"+"
DecimalIntegerLiteral("1") ←
}
}
"*"
DecimalIntegerLiteral("2") ←
}Lisp was the first language I used (other than assembly code) so when I encountered C I was puzzled why they even bothered to have statements at all. 40 years later I still don't understand the point.
1 2 3
which are three expressions, and probably the programmer meant "123". Of course: 1; 2; 3;
is OK. (+ 1 2)
as opposed to: 1+2
Whether the gain is worth it is, I agree, debatable.In C, `1+2` is an expression that returns 3. `1+2+3` acts like two expressions (and perhaps it is; I forget the precise terminology used in the standard). You can write `return foo(1+2)`.
But `for` introduces a statement so can only be used where a statement can.
To me, this indicates there is some merit to the statement/expression distinction.
int foo(...) {
if (condition) return an_expression;
...
return another_expression;
}
You'd put everything after that if into the else part instead of dropping it (like you would in C): (defun foo (...)
(if condition
an-expression
(progn
...
another-expression))) ;; and this /progn/ form would possibly be extracted to a new function if it was too long
Or more likely a cond if using Common Lisp and there were more than two cases: (defun foo (...)
(cond (condition an-expression)
...
(t
...
another-expression)))
And in Common Lisp (and some others) you can still get a return form, using return-from in particular, from functions if you want to keep something more like the C-ish form: (defun foo (...)
(when condition (return-from foo an-expression))
...
another-expression))The first few examples are what I was more familiar with, and it is precisely your note “and this /progn/ form would possibly be extracted to a new function if it was too long” which makes me want to just return early in some cases. Sometimes a case is common enough that it warrants a separate function, but having to define functions for incredibly specific cases just to avoid excessive indentation is not ideal for me.
At least in SBCL, with it compiling the function, it turns into exactly the kind of assembly code you'd expect. Using disassemble:
CL-USER> (defun foo (n)
(when (< n 0) (return-from foo 20))
(* 20 n))
CL-USER> (disassemble #'foo)
; disassembly for FOO
; Size: 54 bytes. Origin: #x53642F41 ; FOO
; 41: 498B4510 MOV RAX, [R13+16] ; thread.binding-stack-pointer
; 45: 488945F8 MOV [RBP-8], RAX
; 49: 488B55F0 MOV RDX, [RBP-16]
; 4D: 31FF XOR EDI, EDI
; 4F: FF14252001A052 CALL QWORD PTR [#x52A00120] ; SB-VM::GENERIC-<
; 56: 7C16 JL L1
; 58: 488B55F0 MOV RDX, [RBP-16]
; 5C: BF28000000 MOV EDI, 40
; 61: FF14251001A052 CALL QWORD PTR [#x52A00110] ; SB-VM::GENERIC-*
; 68: L0: 488BE5 MOV RSP, RBP
; 6B: F8 CLC
; 6C: 5D POP RBP
; 6D: C3 RET
; 6E: L1: BA28000000 MOV EDX, 40
; 73: EBF3 JMP L0
; 75: CC10 INT3 16 ; Invalid argument count trap
This sets up the call for the comparison, conditionally jumps based on the result (JL), and then returns one of the two results. Note that the "40" in 5C and 6E are really "20", this is a consequence of how many CL implementations handle integers, the low-order bit is used as a tag so not really part of the value. If you wrote it in C it would be very similar.https://godbolt.org/z/cYPhdW3qd
The biggest difference is that the C version doesn't need a call for the comparison.
These are expressions:
https://github.com/estree/estree/blob/master/es5.md#expressi...
These are statements:
https://github.com/estree/estree/blob/master/es5.md#statemen...
I guess the confusing part for many is how an expression can also be a statement. But if you look at the ExpressionStatement you see that an expression is NOT also a statement. It's just the wrapper statement that makes it look so!
My understanding is this; Expressions return a value, statements on the other hand are actions, commands, definitions, declarations, assignments, and control structures.
`square = square(4)` is a statement (a definition to be exact) that associates name `square` with the value `square(4)`.
The name `square` itself, when later used, is an expression, because when you call it, it evaluates to a value `square(4)`.
foo = if (x < y) {
...
...
}
Doesn't work because statements don't yield values. let foo =
if x < y
somevalue
else
someothervalue
in f# for example.This is certainly another great aspect of it.
const x = x < y ? 2 : 3;What is sometimes confusing, is that some statements also double as expressions, that is, also return values; and that it also depends on the language. eg. the assignment statement returns a value in C and Javascript, but not in Go or Python;
printf("%d\n", a = 42);
// out: 42
console.log(a = 42)
// out: 42
fmt.Printf("%d\n", a = 42)
// out: syntax error: unexpected =, expecting comma or )
print("%d" % a = 42)
# out: SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
Also depending on the language, expressions themselves may be valid statements. For example, this compiles in C; int main() {
12 + 42;
}
;this is a valid python program: 12 + 42
;but if I try this in Go: package main
func main() {
42
}
// ./main.go:3:5: 12 + 42 (untyped int constant 54) is not usedI may be wrong but I actually think the explanation can be a little simpler than the article posits: an expression (in JS) is something that returns a value. That's it. The value can be undefined/void, but it's still a value being returned.
One important detail the article seems to miss that I've come across whie explaining JS syntax to JSX-learners is that, while statements usually contain expressions, expressions can also contain statements. This is because a function definition returns a value (the function instance) & obviously function bodies can contain any JS syntax. This is useful if you (for some odd reason) want to embed a statement in some JSX code (since JSX only supports JS expressions).
I do think mandatory semicolons make the distinction a little easier. If it has a semicolon, it's probably a statement. And for most C-style languages if it has curly braces, it's probably a statement too. Otherwise it's an expression.
Really? With lit you can create a component that has several top-level elements[1].
EDIT: In fact this works even in react/preact with htm[2], so it's just a limitation of JSX.
[1]: https://lit.dev/playground/#project=W3sibmFtZSI6InNpbXBsZS1n...
But in the beginning it's hard for people to grok an expression with a couple of nested expressions in it. Especially if there's some functions calls in there as well.
Axel Rauschmayer has a good page (1) on this topic as well. Quite dry, so less beginner friendly.
So what I like about Josh W Comeau's article is that it's easy to read with lots of examples and even an interactive widget.
Oh and about interactive visualization of expression evaluation: Thonny (2) does this real nicely for Python code. Excellent for beginners imo.
[1] https://2ality.com/2012/09/expressions-vs-statements.html
const MyComponent = () => {
return (
<>
<h1>Hello world</h1>
{ arr.map(val => <ListItem val={val} />) }
<h1>Goodbye world</h1>
</>
);
}
Curly braces often denote expressions. They're everywhere:In functions:
const x = () => {
};
// or
function myFunc() {
};
In conditionals (while the conditionals are statements their bodies are expressions) if (x > 2) {
return true;
}
In Loops while (x > 2) {
doSomething();
}
etc.This is obviously part of the language grammar of many languages and makes it obvious why React uses curly braces as well for interpolating values.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
In fairness it's difficult to talk about statements accurately and succinctly without being more abstract - and perhaps the more general observation here is how explanation by example alone is not always sufficient. A more general and yet tangible starting point for statements in JS could be "anything ending in a semicolon" (not quite true but almost for normal code), which at least is better at highlighting it's breadth before piling into arbitrary examples.
With this definition, it's easy to see that, if the syntax starts with a language keyword, it's a statement.
BTW, language functions (like 'Array.join()') are expressions because they don't start with a keyword.
For example, in C# 7, throws were made into expressions to allow syntax like this:
return a ? b : throw new Exception();
So I wouldn't rely on this rule.I think that, in the context of JavaScript, if you want a "quick and dirty" way to differentiate them, the `console.log( /* put thing here */ ) ` idea from the article is the best way to go - if you can validly put the thing there, it's an expression, otherwise it's a statement.
a = alert("hello"); // Assigns a to undefined
What isn't an expression? An `if` statement is not an expression. a = if (true) alert("hello"); // Uncaught SyntaxError: Unexpected token 'if'A JS program is composed out of statements, which may contain expressions. In particular, in JS, any expression is also valid as a statement. This is not true in all languages (for example, `2` is not a valid statement in Go).
I do agree!
Sorry to bother you but I am a bit confused about this part of your comment:
> `alert("Hello, World!")` or `doSomething()` are usually statements, not expressions;
Can you please clarify what you mean here? Do you mean `alert("Hello, World!")` is an expression? Or do you mean it is not an expression? Or did you mistype?
Your `console.log( /* put thing here */ )` test is a pretty good one and indeed `console.log(alert("Hello, World!"))` passes this test.
function foo() {
alert("Hello, World!");
doSomething();
}
And not as expressions (as in your example of `console.log(alert("Hello, World"))`.Btw, a better rule of thumb for JS is "if you can add `;` after it and not alter the meaning of the program, it's a statement; otherwise it's an expression".
mov eax ebx
jump 1235
xor eax eax
The first big programming language with expressions was Fortran. Its expressions were for mathematical formulas. Everything else was a statement that you still had to write one per line (and in under 80 columns, to fit in a punch card).Part of the reason fortran was like this is that parsing algorithms were quite primitive. Ideas such as lexing, "tokens", and context-freem grammars, which are standard today, weren't common back then. For example both of the following were valid ways to start a loop in Fortran. Note the lack of whitespace in the second!
do i = 1, 10
doi = 1,10
Moving on, as compiler writers got better at parsing it became more popular to allow free formatting in programming languages. Instead of mandating one statement per line, use semicolons as statement terminators and let you format it as you see fit. Over time this led to a blurrying of the distinction between statement and expression. For example, in C and Javascript many things that used to be statements are now expressions (one notable example being assignments). This is taken the furthest in functional programming languages, where almost everything is an expression.https://news.ycombinator.com/item?id=31944352
For example, functions that return functions:
const filter = (predicate, xs) => xs.filter(predicate)
So it's an assignment, leading me to think it's a statement, but then using filter later would be an expression?
function filter(predicate, xs) { return xs.filter(predicate) }
lets you know immediately that “filter” is a function, not a value. There are valid cases where assigning an anonymous function a name via “const” is appropriate, for example when working with higher-order functions, but most of the time it’s not necessary or helpful. I wish it hadn’t caught on.EDIT: I should have read your comment more carefully. The example function is not a HOF though, it runs xs.filter(pred) when called. A HOF would be
function addConst(x) ( return (y) => x + y }
const add3 = addConst(3)
add3(5) // returns 8
The second line can still be written as function add3(x) { return addConst(3) }
I think people use the const form because there’s less text to write. Syntax highlighting makes this form more bearable, but I still don’t like it. let x = <something>;
If this compiles, <something> is a statement: <something>;
Both, either, or neither can be true.`while (true) { puts("Hello World\n"); };`
let x = <something>;
compiles but <something>;
does not compile. I think all expressions are statements.It can be both a statement and an expression. Yes.
It can be neither a statement nor an expression. Yes.
It can be a statement but not an expression. Yes.
But the case that it is an expression but not a statement: this case never happens.
ones = 1 : ones
take 3 ones // [1,1,1]
odds = 1 : map (+2) odds
take 3 odds // [1,3,5]
Haskell has a statement-like “do” notation, but it is a syntactic sugar for a structure of specific operators.Lisp is also a beast to be aware of, though it is not exactly what you described.
No. The expression before evaluation (i.e. as it's written) is a question/query/ask, after a successful evaluation you get a value - an answer. In case when the evaluation failed, you get either a compilation/parsing error, runtime error/exception, crash, infinite loop/no return, or undefined behavior.
--
- Commands and Queries (as in CQS[1] and CQRS[2])
- Wishes and Asks (it's a wish b/c unlike command it can be rejected, and for the same reason it's an ask and not a query - i.e. "you may ask, but it doesn't mean I'll answer").
- Truth or dare?[3] (Dares and Truth-es ;)
--
[1] https://en.wikipedia.org/wiki/Command%E2%80%93query_separati...
Sure, syntactically it's an expression-based language, but some of those are expressions in syntax only. If a function terminates in nil, what exactly did it compute? And what exactly is the point of all but the last argument to a `progn`?
An expression that is evaluated for the side effects, not the result value, is a statement in every way that matters.
(Haskell has a stronger claim because it actually captures the side effects as a value.)
I think the general consensus is that expressions evaluate to values in unexceptional situations, full stop. Whether they have side effects is immaterial. Whether the value returned is useful or not is also immaterial. Perhaps your examples weren't the best, since both setf and loop evaluate to values which are often useful.
This isn't just Lisp. For example, the C99 specification explicitly declares that all function calls are expressions, even if they return void.
Wikipedia also defines an expression (vs. statement) unambiguously as "a syntactic entity in a programming language that may be evaluated to determine its value.... Expression is often contrasted with statement—a syntactic entity that has no value (an instruction)."
nil.
The fact that you can syntactically put loops and ifs in place of expressions is what makes them expressions
One would think you could just `(defun fun () nil)`.
I have the impression that you think that an expression is a way to compute a value, while I would posit that an expressions are a method of combining values.
Not really an expression:
a0 = 1;
a1 = f(2);
if (a0 < a1){
a2 = getInput();
} else {
a2 = 3;
}
result = a0 + a1 + a2;
More of an expression: result = 1 + f(2) + (a0 < a1 ? getInput() : 3);