What's with this weird looking syntax with an ampersand and a symbol
When &:object_id
is used as an argument to a method call, it will convert the symbol :object_id
into a Proc
instance with the Symbol#to_proc
method.
This syntax is really useful when mixed with Enumerable
methods such as map
and select
:
irb(main):001:0> [ 1, 2, 3, 4, 5 ].select(&:even?)
=> [2, 4]
irb(main):002:0> [ 1, 2, 3, 4, 5 ].map(&:even?)
=> [false, true, false, true, false]
The result of :even?.to_proc
is a Proc
instance, very similar to proc { |x| x.even? }
with one major difference. They handle arguments differently. A proc is not as strict as a method, you give it too many arguments, it ignores them, you don't give enough, it fills the missing values with nil
:
irb(main):001:0> a_proc = proc { |a, b, c| p a, b, c }
irb(main):002:0> a_proc.call(1)
1
nil
nil
=> [1, nil, nil]
irb(main):003:0> a_proc.call(1,2,3,4)
1
2
3
=> [1, 2, 3]
We can inspect the arity, to see the number of parameters:
irb(main):004:0> a_proc.arity
=> 3
And the Proc#parameters
method confirms that all three are optional:
irb(main):005:0> a_proc.parameters
=> [[:opt, :a], [:opt, :b], [:opt, :c]]
Note that Lambdas, beside handling return
and break
differently, treat their arguments as required:
irb(main):006:0> a_lambda = lambda { |a, b, c| puts a, b, c }
irb(main):007:0> a_lambda.arity
=> 3
irb(main):008:0> a_lambda.parameters
=> [[:req, :a], [:req, :b], [:req, :c]]
On the other hand, the result of Symbol#to_proc
is an interesting hybrid. We can confirm that it is not a lambda:
irb(main):017:0> :even?.to_proc.lambda?
=> false
But it handles arguments differently than a regular block, let's first check its arity:
irb(main):020:0> :even?.to_proc.arity
=> -1
irb(main):022:0> :even?.to_proc.parameters
=> [[:rest]]
-1
is what you'd expect from the following Proc
, one that accepts any number of arguments, but we can see that the result of parameters
is different. Symbol#to_proc
creates a block with a nameless parameter.
irb(main):001:0> proc { |*a| p a }.arity
=> -1
irb(main):002:0> proc { |*a| p a }.parameters
=> [[:rest, :a]]
We can get close to the to_proc
result with the following, but there's not much we can do with a nameless parameter, sometimes called "naked parameter". This can be useful with methods, as calling super
will forward all the arguments to the parent method, but there's no parent method with a block!
irb(main):001:0> proc { |*| }.parameters
=> [[:rest]]
Back to the result of Symbol#to_proc
, we get exceptions back with no arguments, or more than one:
irb(main):023:0> :even?.to_proc.call
Traceback (most recent call last):
4: from /Users/pierre/.rbenv/versions/2.7.1/bin/irb:23:in `<main>'
3: from /Users/pierre/.rbenv/versions/2.7.1/bin/irb:23:in `load'
2: from /Users/pierre/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
1: from (irb):23
ArgumentError (no receiver given)
irb(main):024:0> :even?.to_proc.call(1, 2)
Traceback (most recent call last):
6: from /Users/pierre/.rbenv/versions/2.7.1/bin/irb:23:in `<main>'
5: from /Users/pierre/.rbenv/versions/2.7.1/bin/irb:23:in `load'
4: from /Users/pierre/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
3: from (irb):23
2: from (irb):24:in `rescue in irb_binding'
1: from (irb):24:in `even?'
ArgumentError (wrong number of arguments (given 1, expected 0))
One argument is required, so that the method identified by the symbol's value can be called on it, Kernel#object_id
and Integer#even?
in the examples above, and remaining arguments are forwarded to the method. In this case even?
does not accept any and we get an ArgumentError
exception with the cause wrong number of arguments (given 1, expected 0)
.
It's not just for parameter-less methods
A common mistake I've seen is to think that this approach limits us to parameter-less methods, such as Integer#even?
, the following is an example using the "spaceship operator" method, which is defined as :
int <=> numeric → -1, 0, +1, or nil
And the sort method has the following method signature according to the Ruby docs:
sort { |a, b| block } → array
irb(main):004:0> [ 5, 3, 1, 2, 4 ].sort(&:<=>)
=> [1, 2, 3, 4, 5]
The method <=>
was called with two arguments, the two array elements it needs to compare, it is very close to the following more explicit approach:
irb(main):004:0> [ 5, 3, 1, 2, 4 ].sort { |a, b| a.<=>(b) }
=> [1, 2, 3, 4, 5]
We can pass almost anything after the ampersand
The call to to_proc
is triggered in the first place because when handling a method call, Ruby needs to make sure that if it received a block argument, that this argument is actually a proc.
The ampersand character has itself nothing to do with the symbol, or whatever comes after it. The ampersand's role in an argument list is to identify the block argument, which must come last if present.
Note that while it is very common to see &
used with symbols, and procs themselves when passed as block arguments to a method, in true duck-typing fashion, anything that responds to to_proc
will work. For instance Hash#to_proc
returns a proc that accepts one argument and returns either nil
or the value
at the key identified by the one argument:
irb(main):001:1* def a_method(&block)
irb(main):002:1* block.call(1)
irb(main):003:0> end
=> :a_method
irb(main):004:0> a_method(&{ 1 => 'one' })
=> "one"
irb(main):005:0> a_method(&{ 2 => 'two' })
=> nil
And we could pass our own class, as long as it responds to to_proc
:
irb(main):004:1* class A
irb(main):005:2* def self.to_proc
irb(main):006:2* proc { puts "Not really useful but it works" }
irb(main):007:1* end
irb(main):008:0> end
=> :to_proc
irb(main):009:0> a_method(&A)
Not really useful but it works
=> nil
There are four classes defined with a to_proc
method in the standard library: Symbol
, Method
, Proc
and Hash
. Enumerator::Yielder
is not counted as a "standard library" class given its nature as an implementation detail of Enumerator
.
Links:
Thank you to Étienne Barrié for pointing me to the following links. Rails used to have its own definition of Symbol#to_proc
, for when it was supporting Ruby 1.8.x. The method we've talked in this chapter was added in 1.9.x. Rubinius also has its own definition.
- Rubinius: https://github.com/rubinius/rubinius/blob/v5.0/core/symbol.rb#L149-L152
- Rails 2.3:18: https://github.com/rails/rails/blob/v2.3.18/activesupport/lib/active_support/core_ext/symbol.rb
Conclusion
A block created through Symbol#to_proc
requires at least one argument, the receiver of the method, and the remaining arguments must match the arity of the method identified by the symbol itself.
even?
is parameter-less, &:even?.to_proc
must be called with one argument only, the receiver. <=>
has an arity of one, :<=>?.to_proc
must be called with two arguments, the receiver, and the one and only argument to <=>
.
Wondering how Ruby actually obtains the desired result given that it's dealing with nameless parameters? Let's look at the C definition of the method:
VALUE
rb_sym_to_proc(VALUE sym)
{
}
If you think it's weird, that's because it is!
In a next article, we'll look at how exactly Ruby handles block arguments when converting a symbol to a proc, we'll explore MRI's source code, in C, and peek at the grammar definition of the Ruby language. It's gonna be fun!
Found this interesting? You'll enjoy my free online book about rebuilding Redis, in Ruby.
Appendix, Arguments vs Parameters
As of recently I used to use the terms "arguments" and "parameters" interchangeably, and I was wrong.
Parameters are the variables that you can use from withing a function/method/block, in the following example, a
and b
are parameters:
def a_method(a, b)
end
Arguments are the values that are given to a function/method/block, that will be be bound to the parameter variables. In the following example, 'a'
and 20
are arguments:
a_method('a', 20)
Top comments (0)