Symbol#to_proc with multiple arguments: A hidden power-feature of Ruby

posted 2012-Jan-24
— updated 2012-Feb-3

In Ruby’s syntax, putting an ampersand (&) before the last parameter in a method call causes a to_proc method to be called on it and the result to be used as the block for the method invocation. Originally, this was largely used for storing blocks as Procs inside the method, and then turning them back into blocks when calling another method.

Ruby 1.9 added a to_proc instance method to the Symbol class, known as Symbol#to_proc. The implementation of this method causes the object yielded to the block to have the method named by the symbol to be invoked on it. For example, let’s say that we have an array of floating-point numbers and we want to find the closest integer value for each:

numbers = [ 1.23, 4.56, 7.89 ]

# In Ruby 1.8
nearest = numbers.map{ |n| n.round }

# In Ruby 1.9
nearest = numbers.map( &:round )

This allows for code that’s easier to type, easier to read (once you know what you’re looking at), and is generally better.

Today, however, I found out that it is even more powerful. If more than one parameter is passed to your block, the proc created by Symbol#to_proc uses the additional block parameters as parameters to the method call. If you have code that looks like this:

some_method do |foo, bar, baz|
  foo.swizzle( bar, baz )
end

…then you can rewrite it in Ruby 1.9 as just:

some_method(&:swizzle)

Where do we often see blocks that yield two parameters? Why, in our good friend Enumerable#inject!

values = [ 1, 2, 3, 4, 5, 6 ]
total = values.inject(0){ |sum, num| sum + num } # This is far too much code
total = values.inject(0,&:+)                     # Sweet!

I have a feeling that this will prove useful beyond impressively terse code. The one damper on this parade, however, is that you must have two values actually being yielded to your block. Yielding an array that has two values is not good enough:

# This works, restructuring the two-valued array as two block arguments
players.zip(turn_scores).each{ |player,turn_score| player.add_score(turn_score) }

# This, however, fails
players.zip(turn_scores).each(&:add_score)
#=> NoMethodError: undefined method `add_score' for [<#Player 'Gavin'>, 42]:Array

The Solution

All is not lost, however. Since all the Enumerable methods return an Enumerator if you call them without a block, we can monkeypatch that class to yield all values explicitly:

class Enumerator
  def splatted
    each{ |a| yield(*a) }
  end
end

With this, we can now do some wonderful things:

# Sum each pair of consecutive integers
p (1..10).each_cons(2).map.splatted(&:+)
#=> [3, 5, 7, 9, 11, 13, 15, 17, 19]

# What is the largest integer resulting from raising one single-digit integer to the power of another?
p (1..9).to_a.permutation(2).map.splatted(&:**).max
#=> 134217728
Michael Kohl
09:42AM ET
2012-May-04

Nice post, however inject is maybe not the best example, since it already directly takes a symbol argument (and the 0 isn’t necessary): [*1..5].inject(:+)

net.mind details contact résumé other
Phrogz.net