Ruby & to_proc

AuthorMáximo Mussini
·3 min read

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 call to_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 defines Symbol#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.