Ruby 2.7 — Numbered Parameters
Ruby 2.7 is coming out this December, as with all modern releases, but that doesn’t stop us from looking for and writing about all the fun things we find in the mean time! No no no.
For this article, we have something that’s very reminiscent of Bash, Perl, and Scala: Numbered parameters.
NOTE: This syntax was updated from
_1
to_1
, the article will be updated to account for this.
The Short Version
If you have a simple block with positional arguments, especially single positional, you can do the following:
[1, 2, 3].map { _1 + 3 }
=> [4, 5, 6]
…where _1
is the first parameter to the block function.
The Discussion
If you’d like to see the discussion behind this feature, it can be found here:
Feature #4475: default variable name for parameter - Ruby trunk - Ruby Issue Tracking System
Examples
As this exposes the object directly, a majority of the examples are just slight adjustments to what you might be familiar to already. Let’s take a look at some of the examples in the test code:
Numbered parameters [Feature #4475] · ruby/ruby_12acc75
Valid Only In Blocks
Numbered parameters are only valid when referenced inside of a block:
assert_syntax_error('_1', /outside block/)
assert_valid_syntax('proc {_1}')
That means this is valid:
proc { _1 }
=> #<Proc:0x0000000111d42990@(pry):6>
…and this will cause a syntax error:
_1
SyntaxError: (eval):2: numbered parameter outside block
Now the error is slightly modified from older Ruby versions in that it recognizes the second-use of the instance variable-like syntax and lets us know we used it outside of a block.
Naming Rules
A numbered param has to follow a few rules, namely there are only numbers in it and 0 along with leading 0s are errors:
assert_syntax_error('proc {_01}', /leading zero/) assert_syntax_error('proc {_1_}', /unexpected/)
That also means it’s going to do bad things if you try and use underscores for longer numbers:
(1..1_000_000).each_slice(1_000).map { _1 + _1_000 + _2 + _3 }
SyntaxError: unexpected local variable or method, expecting '}'
..._slice(1_000).map { _1 + _1_000 + _2 + _3 }
... ^~~~
(eval):2: numbered parameter outside block
...e(1_000).map { _1 + _1_000 + _2 + _3 }
...
Multiple Numbered Parameters
Say we have collections or even hashes, we can use _2 and further if we need them to get at the specific values:
assert_equal(3, eval('[1,2].then {_1+_2}'))
assert_equal("12", eval('[1,2].then {"#_1#_2"}'))
For hashes this means you can access the key and the value:
{name: 'foo', age: 42}.map { [_1, _2] }
=> [[:name, "foo"], [:age, 42]]
If you had groups of three you could even start using more:
(1..9).each_slice(3).map { _1 + _2 + _3 }
=> [6, 15, 24]
…though it may be ill-advised to start getting into too many of these numbered params, as eventually you run out.
Too Large of a Number
There is a limit, but it’s rather high:
assert_syntax_error('proc {_9999999999999999}', /too large/)
Defined here in this constant:
#define NUMPARAM_MAX 100 /* INT_MAX */
Though there should be warning signs that you’re doing something odd before you get anywhere close to this number.
Currently Pry will just give up if you try, and expect more input:
[13] pry(main)> (1..1_000_000).each_slice(1_000).map { _101 }
[13] pry(main)*
Ordinary Parameters
Ruby doesn’t like mix-and-match with our current way of doing block parameters:
assert_syntax_error('proc {|| _1}', /ordinary parameter is defined/) assert_syntax_error('proc {|x| _1}', /ordinary parameter is defined/)
If you decide to use this, know that it’s one or the other, not both.
Hashes and Objects
It would be good to remember that _1 and friends are just Ruby objects, meaning we can call anything on them that we would a parameter:
[{name: 'foo'}, {name: 'bar'}].map { _1[:name] }
=> ["foo", "bar"]
[{name: 'foo'}, {name: 'bar'}]
.map { OpenStruct.new(_1) }
.map { _1.name }
=> ["foo", "bar"]
Though in the last example, it would be good to remember the current shorthand syntax of map(&:name)
instead.
Wrapping Up
This is definitely a very interesting feature, though I certainly feel Matz when he says the following:
I still feel weird when I see @ and @1 etc. Maybe I will get used to it after a while.
I need time.
- Matz.
NOTE: The syntax was updated from
@1
to_1
, the above comment was updated to reflect this.
I wonder what new things it will lead to, but I’m excited nonetheless to see what else people can use it for.
2.7 is already off to an interesting start, let’s see where it goes from here.
Top comments (1)
I need time too, honestly is a cool feature but I can't find a use case for this.
It will be nice to have at least some real world examples for this new feature.