Skip to content

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

  1. Write five blocks three ways — |x|, _1 and it — for map, select, sort_by and sum over [3, 1, 2], and print that all three spellings agree. Then add a pairwise sum over [[1, 2], [3, 4]] and show why its it version has to read it.sum while its numbered version reads _1 + _2.
  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 -w to 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.

named:    [[6, 2, 4], [3, 1], [3, 2, 1], 60]
numbered: [[6, 2, 4], [3, 1], [3, 2, 1], 60]
it:       [[6, 2, 4], [3, 1], [3, 2, 1], 60]
all three agree: true
pairs with |a, b|: [3, 7], with _1 + _2: [3, 7]
pairs with it, which is the whole pair: [3, 7]

See also