it and _1 name the block's parameter¶
Level: 201 · anyone reading Ruby 3.4+ code in which a block never declares |x|
One line: A block with no |x| can read its arguments as _1, _2, … (Ruby 2.7) or as it (Ruby 3.4); they are parameters rather than variables, so the parser refuses to mix them, to nest _1, or to put them beside an ordinary parameter — and outside a block it is an ordinary name while _1 is reserved.
A block that declares no parameters can still read them. _1 is the first argument and _2 the second (rows 1 and 3), and Ruby 3.4 added it for the one-argument case (row 2). Using _2 makes the block a two-parameter block, so a pair yielded to it is spread across _1 and _2, while it or a lone _1 is a one-parameter block that takes the whole pair (row 4); arity and parameters report exactly that, with _1 and _2 appearing as named optional parameters, it as an anonymous one, and both required inside a lambda (row 5). Because they are parameters and not variables, the parser polices them: an it block may contain another it block, each it meaning its own block's argument, but a _1 block may not contain another _1 block (row 6); it and _1 cannot share a block (row 7); and neither may appear beside an ordinary |x| (row 8). Outside a block it is an ordinary identifier — which is a trap, because a local variable named it wins over the implicit parameter in every block below it — while _1 cannot be assigned at all (row 9); and it in a method body with no block and no local is a plain method call, so it is a NameError (row 10).
Verified output of it_and_numbered_parameters_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. _1 names the first parameter: [1, 2].map { _1 * 2 } = [2, 4]
2. it (Ruby 3.4) names it too: [1, 2].map { it * 2 } = [2, 4]
3. _2 makes a two-parameter block: { _1 + _2 } = [3, 7], each_with_index.map { _1 * _2 } = [0, 2]
4. it and a lone _1 take the whole pair, _2 splats it: it.size [2], _1.size [2], _1 + _2 [3]
5. arity and parameters: proc { it } 1 [[:opt]], proc { _2 } 2 [[:opt, :_1], [:opt, :_2]], lambda { it } [[:req]]
6. nested it is allowed, nested _1 is not: [[10, 20], [30]], SyntaxError
7. it and _1 in one block: SyntaxError
8. it or _1 beside an ordinary |x|: SyntaxError, SyntaxError
9. it is an ordinary name elsewhere, _1 is reserved: a local it wins in a block: ["local"], no local: [10], _1 = 5: SyntaxError
10. it in a method body, no block: NameError: undefined local variable or method 'it' for main
Reading the output¶
Rows 1 and 2 give [2, 4] twice: _1 and it are two spellings of the same one-parameter block. Row 3 adds _2 and the pairs are summed to [3, 7]; with each_with_index the value is _1 and the index _2, hence [0, 2]. Row 4 is the splat rule made visible on the single pair [1, 2]: it.size and _1.size are 2, because each received the whole array, while _1 + _2 is 3, because mentioning _2 split it. Row 5 prints arity 1 and parameters [[:opt]] for proc { it }, arity 2 with two named optionals for proc { _2 }, and [[:req]] for lambda { it }. Row 6 prints [[10, 20], [30]] from an it inside an it, then SyntaxError for _1 inside _1; rows 7 and 8 are three more SyntaxErrors, each compiled from a string. Row 9 prints ["local"] from a method that assigned it = "local" before its block and [10] from the same block in a method without that local, then SyntaxError for _1 = 5. Row 10 is the NameError whose message says what it is when there is no block: an undefined local variable or method.
Compared with Python¶
Python has no implicit parameter, and nothing the parser needs to police: a lambda and a comprehension both name their variable (rows 1 and 2), two values are taken apart by naming both (row 3), and where Ruby chooses between the whole pair and a splat by writing _2 or not, Python chooses by writing p or a, b — a lambda has had no tuple-parameter unpacking since Python 3, so the whole pair is p and its parts are p[0] and p[1] (row 4). inspect.signature and co_argcount describe the parameter list (row 5), nested comprehensions each name their own variable so nothing can clash (row 6), and it and _1 are plain identifiers (rows 7 and 8): lambda x, it: it is simply a two-parameter lambda. A lambda that mentions it compiles with it as a free name, and calling it after del it is a NameError (row 9) — the outcome of Ruby's row 10, reached because a name is unbound rather than because a method is missing. Row 10 is the nearest thing Python has to a magic short name, _: in a script it is an ordinary variable; the interactive prompt keeps the last value in it, and match reads it as a wildcard, two conventions that do not touch each other (Not machine-checked here.). The pieces are documented under lambda expressions ↗, comprehensions ↗ and the wildcard pattern ↗.
Verified output of it_and_numbered_parameters_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. a lambda names its parameter: map(lambda x: x * 2, xs) = [2, 4]
2. a comprehension names it too: [x * 2 for x in xs] = [2, 4]
3. two values are unpacked by name: [a + b for a, b in pairs] = [3, 7], enumerate: [0, 2]
4. one parameter takes the whole pair, unpacking is spelled out: len(p) [2], p[0] + p[1] [3], a + b [3]
5. signature and argcount: lambda x: x (x) 1, lambda a, b: a (a, b) 2
6. nested comprehensions name their own variable: [[10, 20], [30]], no clash possible
7. it and _1 are plain names, nothing to mix: _1 = 5; it = 6 -> 5 6 (int)
8. it beside an ordinary parameter is a parameter: lambda x, it: it -> (x, it), called (1, 2) = 2
9. a lambda mentioning it compiles; it is a free name: co_names ('it',), called after del it: NameError
10. _ is an ordinary name in a script: _ = 7 -> 7; the REPL's last value and match's wildcard are separate conventions
Try it¶
- Write five blocks three ways —
|x|,_1andit— formap,select,sort_byandsumover[3, 1, 2], and print that all three spellings agree. Then add a pairwise sum over[[1, 2], [3, 4]]and show why itsitversion has to readit.sumwhile its numbered version reads_1 + _2. - Put
it = "trap"at the top of a script that uses{ it * 2 }further down and watch every block read the local; then move the blocks into a method so the parameter comes back. Decide which of the two you would want-wto warn about, and check whether it does.
Solution to 1
it_and_numbered_parameters_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: five blocks written three ways -- |x|, _1 and it -- agree on
# every input; the pairwise one reads differently with `it`, because `it` is one
# parameter holding the whole pair, while `_1 + _2` are two parameters.
xs = [3, 1, 2]
named = [xs.map { |x| x * 2 }, xs.select { |x| x.odd? }, xs.sort_by { |x| -x }, xs.sum { |x| x * 10 }]
numbered = [xs.map { _1 * 2 }, xs.select { _1.odd? }, xs.sort_by { -_1 }, xs.sum { _1 * 10 }]
with_it = [xs.map { it * 2 }, xs.select { it.odd? }, xs.sort_by { -it }, xs.sum { it * 10 }]
puts "named: #{named}"
puts "numbered: #{numbered}"
puts "it: #{with_it}"
puts "all three agree: #{named == numbered && numbered == with_it}"
pairs = [[1, 2], [3, 4]]
puts "pairs with |a, b|: #{pairs.map { |a, b| a + b }}, with _1 + _2: #{pairs.map { _1 + _2 }}"
puts "pairs with it, which is the whole pair: #{pairs.map { it.sum }}"
Verified output of it_and_numbered_parameters_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
&:symcallsSymbol#to_proc— the other short spelling of{ |x| x.upcase }, and the one that takes no expression- Blocks are not objects — the block these parameters belong to
- Procs and lambdas differ in arity and
return—arityandparameters, which row 5 reads - Closures capture variables, not values — why a local named
itreaches into the blocks below it - Destructuring assignment — the splat of row 4 in its general form
- Ruby versions: what changed —
_1in 2.7 anditin 3.4 on the timeline - Rust: what a closure is ↗ — a closure that always names its
|x| - Ruby shows you its bytecode and its syntax tree —
RubyVM::InstructionSequence.compile(src).disasmlists the YARV instructions the interpreter will execute…