Symbol to Proc shorthand

— March 7, 2006 at 17:02 PST


Rails 1.1 adds a conversion method Symbol#to_proc that allows specifying a mapping by using a symbol instead of a block. Here's an example:

[1, 2, 3].map(&:to_s)

is equivalent to

[1, 2, 3].map { |i| i.to_s }

and of course can also be written

message = :to_s
[1, 2, 3].map(&message)

Here's what this does.

[1, 2, 3].map(&:to_s)   # => ["1", "2", "3"]

My first reaction to seeing this was to wonder what the heck that odd ampersand syntax was. I did some digging, but wasn't able to figure it out entirely...

The Pickaxe book says:

If the last argument to a method is preceded by an ampersand, Ruby assumes that it is a Proc object. It removes it from the parameter list, converts the Proc object into a block, and associates it with the method.

In fact it repeats that in several places throughout the book. But the converse notation or preceding an argument with an ampersand in a method call is only implied in one place. In Chapter 23, "Duck Typing", section "Standard Protocols and Coercions", it describes the to_proc coercion as "Used to convert an object prefixed with an ampersand in a method call." (Thanks to Chad Fowler for pointing me in the right direction to find that rather obscure reference.)

Discovering that was one of those moments where I really had to sit back and just admire the Ruby language for a moment. The coercion operation is completely polymorphic! Any object preceded by an ampersand in a method call will be converted to a Proc by sending it the to_proc message. Of course, the object must implement a to_proc method, but Rails 1.1 adds just that to Symbol.

class Symbol
  def to_proc
    Proc.new { |obj, *args| obj.send(self, *args) }
  end
end

Pretty nifty.

The new syntax for mapping seems to be nicer for chaining calls. Compare:

[1, 2, 3, 4].find_all(&:even?).map(&:to_s)
[1, 2, 3, 4].find_all {|i| i.even?}.map { |i| i.to_s }

I think I like it.

2 commentsrails

Comments
  1. Roman LE NEGRATE2006-06-01 10:31:21

    Thank you for having clearly explained this impressive trick :-).

  2. Roman LE NEGRATE2006-06-10 14:17:07

    This trick is unfortunately as handy as CPU time inefficient. I made a benchmark: traditional calls (with a block as in obj.each {|e| e.method_name }) are way (by 3.4x) faster than using a symbol that will be automatically converted to a Proc (like obj.each(&:method_name)).

    Here are the results of the speed test (you will find the code used to produce these results below them):

                               user     system      total        real
    Using Symbol#to_proc   2.690000   0.000000   2.690000 (  2.704374)
    Standard call          0.790000   0.000000   0.790000 (  0.786973)
    

    Ruby code:

    require 'benchmark'
    require 'rubygems'
    require 'active_support'
    
    array = [ 1, 'ab', :dog, 3.8 ]
    
    COUNT = 100_000
    Benchmark.bmbm(2) do |test|
      test.report('Using Symbol#to_proc') do
        COUNT.times do
          array.map(&:to_s)
        end
      end
      test.report('Standard call') do
        COUNT.times do
          array.map {|e| e.to_s }
        end
      end
    end
    

Sorry, comments for this article are closed.