Blocks are a very unique part of Ruby's syntax. Let's look at a simple example:
'Jane Jim Jenny'.split.map { |s| s.length }.reduce { |sum, n| sum + n }
# => 12
When performing this kind of functional transformation, it's tedious to write a block to perform a simple method call. An extremely common idiom in Ruby uses symbols to specify the method that should be called:
names.map(&:length).reduce(&:+)
Sweet! 🍰
In Ruby, the ampersand operator &
can coerce an object into a Proc
by calling the to_proc
method if it's defined. More generally, &object
will be evaluated in the following way:
- object is a
Proc
:&
converts it to a block. - object is not a
Proc
:&
tries to callto_proc
on the object, and then converts it to a block.
names.map &:to_s
# is the same than
names.map &:to_s.to_proc
Huh? 😕
It turns out the magic is in how Ruby defines to_proc
for symbols. In recent versions of Ruby, the method is defined in C, but it would look like this in Ruby:
class Symbol
def to_proc
->(obj, args = nil) { obj.send(self, *args) }
end
end
Back to our snippet, let's expand the to_proc
call incrementally until we arrive at the same block we would write by hand:
names.map &:to_s
# We can expand it to
names.map &:to_s.to_proc
# Replacing "to_proc" with the result of calling the method
names.map &->(name, args = nil) { name.send(:to_s, *args) }
# "map" passes a single argument to the block, so we can simplify
names.map &->(name) { name.send(:to_s) }
# Calling the method directly we get
names.map &->(name) { name.to_s }
# Since "&" transforms Procs and Lambdas to blocks, it's equivalent to
names.map { |name| name.to_s }
So there you have it, &
will coerce the :to_s
symbol by calling to_proc
, and then transform the resulting proc or lambda to a block.
There's nothing special about the shorthand
&:method
syntax. Ruby arbitrarily definesSymbol#to_proc
in a way that allows programmers to avoid some boilerplate.
A world of proc 🌎
Now that we understand what is really going on, we could use to_proc
for our own benefit by defining it in our objects and classes.
require 'ostruct'
class Formula
def initialize(formula)
@formula = formula.gsub('^', '**')
end
def apply(variables)
OpenStruct.new(variables).instance_eval(@formula)
end
def to_proc
->(*args){ apply(*args) }
end
end
x2 = Formula.new('x^2 + y^2')
[{ x: 1, y: 1 }, { x: 3, y: 4 }, { x: 5, y: 7 }].map(&x2)
# => [2, 25, 74]
We may also define to_proc
at the class level, allowing us to pass a class as a block:
class Formula
def self.to_proc
->(*args){ new(*args) }
end
end
['x^2 + y^2', 'x + y^3'].map(&Formula)
# => [#<Formula @formula="x**2 + y**2">, #<Formula: @formula="x + y**3">]
A note on performance 📊
Running some benchmarks in Ruby 2.2.3, it seems that there is not an important performance penalty from using to_proc
. I wrote a small benchmark that you can run if you are curious 😃
Summary
There's nothing special about the shorthand &:method
syntax. Ruby defines Symbol#to_proc
in a particular way that allows programmers to avoid some boilerplate, and the &
operator can coerce any object into a block by calling to_proc
.
Symbol#to_proc
is so ubiquitous that there's no harm in using it; most of the times it can help to keep the code terse without any downsides.
However, it's better to stay away from to_proc
in everyday usage, since it is as obscure as it is powerful. Defining to_proc
for custom objects can make it very difficult to reason about the code, which defeats the purpose of using it in the first place.