Everything is an expression, even def and class¶
Level: 101 · a Python programmer who keeps writing statements
One line: Every Ruby construct — if, case, begin, while, def, class, an assignment — evaluates to a value and can sit on the right of =; Python draws a line between statements and expressions and offers exactly three crossings: 1 if c else 2, the walrus :=, and lambda.
Ruby has no separate category of statement. An if yields the value of the branch that ran, a case the value of its when, a begin … end the value of its last line, and a def the name of the method it just defined, as a Symbol. A construct with nothing to yield yields nil: a while loop, an if whose condition fails and has no else, a puts. Because an assignment is itself an expression, x = y = 1 and y = (a = 1) both work, and because a method body or a lambda body is just a sequence of expressions, eval handles a statement sequence and an expression with the same call. The program below assigns each construct to a variable and prints what arrived.
Verified output of everything_is_an_expression_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. if/else as a value -> 1
2. case/when as a value -> "two"
3. begin...end as a value -> 42
4. while loop as a value -> nil; with `break 5` -> 5
hi
5. x = (puts "hi") leaves x = -> nil
6. def greet; end evaluates to -> :greet
7. class Foo; 99; end evaluates to -> 99; an empty class body -> nil
8. chained x = y = 1 gives [x, y] -> [1, 1]
9. nested y = (a = 1) gives [y, a] -> [1, 1]
10. lambda body with two statements -> 2
11. eval("1 + 2") -> 3
12. eval("x = 1; x + 1") -> 2
13. class ...; private def helper; end -> [:helper] (private received the Symbol)
Reading the output¶
Rows 1 to 3 are the three block constructs used as values: the if gave 1, the case gave "two", and the begin … end block — Ruby's try, and also a plain way to group several lines into one expression — gave 42.
Row 4 shows what a loop is worth: nil, because a while has no last-expression to hand back — unless break 5 ends it, in which case 5 is the loop's value. Row 5 is the everyday version of the same fact: puts returns nil, so x = (puts "hi") prints hi and leaves x as nil. (The hi line sits between rows 4 and 5 because puts printed it while row 5 was being assigned.)
Row 6 is the one that surprises Python programmers: def greet; end evaluates to :greet. Row 13 shows what that Symbol is for: private def helper is the ordinary method private receiving :helper as its argument, and Toolbox.private_instance_methods(false) confirms it arrived. Row 7: class Foo; 99; end evaluates to 99, the last expression in its body, and an empty class body evaluates to nil.
Rows 8 and 9: assignment is an expression, so it chains (x = y = 1) and nests (y = (a = 1)) — no special grammar is needed for either. Row 10: a lambda body holds any number of statements, because a body is a sequence of expressions and a sequence is an expression. Rows 11 and 12: one eval evaluates an expression and a statement sequence alike. (Ruby does have a method named exec, but it replaces the current process with another program — nothing to do with evaluating code. (Not machine-checked here.))
Compared with Python¶
Python's grammar has statements and expressions, and only an expression can stand on the right of =. The twin asks the same thirteen questions; where Python has only a statement, it compiles the assignment form with compile() and prints the class of the error — SyntaxError — and then the expression Python offers instead, when it has one. Three crossings exist: the conditional expression 1 if c else 2 (row 1), the assignment expression (a := 1) (row 9, and inside a lambda in row 10), and lambda, whose body is exactly one expression. match (row 2), try (row 3), while (row 4), def (row 6) and class (row 7) are statements and have no value. Row 5 agrees with Ruby: print returns None. Row 12 is why Python has two functions where Ruby has one: eval takes an expression and returns its value, exec takes statements and returns None. Row 13 is the decorator side of row 6: def has no value, so @private cannot receive a name — it receives the function object itself, after the def statement has run.
Verified output of everything_is_an_expression_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. if/else as a value -> 1 (x = 1 if c else 2; statement form: SyntaxError)
2. case/when as a value -> SyntaxError (match is a statement)
3. begin...end as a value -> SyntaxError (try is a statement)
4. while loop as a value -> SyntaxError (while is a statement)
hi
5. x = (print("hi")) leaves x = -> None
6. def greet(): pass evaluates to -> SyntaxError (def is a statement)
7. class Foo: 99 evaluates to -> SyntaxError (class is a statement)
8. chained x = y = 1 gives [x, y] -> [1, 1]
9. nested y = (a = 1) gives [y, a] -> SyntaxError (y = (a := 1) gives [1, 1])
10. lambda body with two statements -> SyntaxError ((lambda: (t := 1) + 1)() gives 2)
11. eval("1 + 2") -> 3
12. eval("x = 1; x + 1") -> SyntaxError (exec("x = 1") returns None)
13. class ...; @private def helper -> ['helper'] (the decorator received the function object)
The Python library has no page on the statement–expression line itself; the language reference on expressions ↗ and PEP 572 ↗ (assignment expressions) are the sources for the three crossings.
Try it¶
- Write a method
describe(n)whose whole body is onecaseexpression returning"negative","zero"or"positive", and call it for -3, 0 and 7. Then assign anifwith noelsewhose condition is false to a variable, and awhileloop that runs three times to another, and print both withinspect. Finally make aneachcall return a value withbreak. - Use the Symbol that
defreturns: inside a class, writeprivate def helper; end, then printFoo.private_instance_methods(false). Then write your own decorator-style method,def logged(name), that takes that Symbol, wraps the method withdefine_methodandalias_method, and use it aslogged def work; end.
Solution to 1
everything_is_an_expression_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: a method whose whole body is one `case` expression, an `if`
# with no `else` whose condition fails, and the value a loop leaves behind.
def describe(n)
case
when n < 0 then "negative"
when n == 0 then "zero"
else "positive"
end
end
[-3, 0, 7].each do |n|
puts format("describe(%2d) -> %s", n, describe(n))
end
verdict = if 1 > 2 then "impossible" end
puts "if with no else, condition false -> #{verdict.inspect}"
count = 0
left_over = while count < 3
count += 1
end
puts "while loop, run #{count} times, evaluates to #{left_over.inspect}"
found = [4, 9, 16].each do |n|
break n if n > 5
end
puts "each with break n -> #{found.inspect}"
Verified output of everything_is_an_expression_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
unless,untiland the modifier forms — the same constructs as one-line modifiers, and the do-while thatbegin … end whilegivesand/orbind lower than=— because an assignment is an expression,x = false or trueis(x = false) or truecase/inpattern matching —case … inis a value too, and=>is a one-line pattern match- The last expression is the value — a method returns its last expression for the same reason an
ifdoes - Setters return the argument — the one place where the value of an expression is not what the method returned
- irb and the REPL — where the value of every expression is printed back at you
- Rust:
ifis an expression ↗ — Rust makes the same choice as Ruby, with a type check on the branches - Rust:
matchexpressions ↗ — the Rust form of acasewith a value - Ruby docs: control expressions ↗ — the reference for
if,case,whileand friends forloops do not make a scope —forandwhileare syntax and open no scope