Symbol#to_proc with multiple arguments: A hidden power-feature of Ruby
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(:+) |