(A Detailed Explanation of) Ruby’s Symbol#to_proc

Must people love a new feature in Ruby called Symbol#to_proc, but there are not many good explanations of it. The concept is actually fairly straightforward, but the Ruby implementation is fairly complex. The following is an explanation that assumes no knowledge of Ruby.

To sum the numbers from 1 until 100 in Ruby, you would write:

(1..100).inject { |acc, n| acc + n }

inject is a left fold or a reduce- it applies the function {|acc, n| acc + n} to each element in a list where acc is an accumulator that contains the result of the last function application.

For such a simple function, all we need is the knowledge that we are adding, which is contained in the symbol +, representing the addition method. Ideally we would like to write:

(1..100).inject( :+ )

The semicolon is how you tell ruby you are using a symbol, which in this case is the name of a method. Of course, you can write a simple sum function if you want. However, this code is more flexible in a very powerful way- we can use other functions then inject and add.

Method calling in Ruby is actually message passing. So instead of calling

 1.+( 2 )  # => 3

You can say

1.send( :+, 2 )   # => 3

So we are not trying to convert a symbol to a proc. What we are trying to do is continually send the message :+ to inject’s built in accumulator, with every member of the list as an argument.

In ruby, the brackets represent a function (actually a closure) that rubyists call a block. A Proc object can be converted to a proc with &. You can convert a different class to a proc (if it supports it) by calling the to_proc method. If you use & on an object that is not a proc, Ruby will try to coerce that object into a block by calling the to_proc method. So we can hack this syntactic shortcut to create

(1..100).inject &:+

So by defining how to coerce the Symbol class to a proc, we can use & to generate a block of code out of a symbol. The to_proc method we define must deal with any arguments yielded to the block. In this case, every value from (2..100) is yielded to the Proc. (1 will be the starting value of the accumulator.) There are minor variation on defining to_proc, but it basically looks like this:

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

proc is a function that creates a new Proc object. obj is the object to be yielded to the Proc, which in the inject example is the inject’s accumulator. The use of *args twice will receive any additional optionally yielded arguments and pass them along in the send method. In our inject example, *args is passing along the n parameter. Finally, obj.send(self) sends a message to obj containing self, the symbol being used (in our example :+). Now we have accomplished the original code with our more succinct version, that we know expands to:

(1..100).inject( &(proc{ |obj, *args| obj.send(:+, *args) }) )

That wasn’t so hard, was it? Actually, it was pretty complex for just trying to pass one function to another, something which is dead simple in functional languages. You can accomplish a lot of things with a closure, including this hack. But to me this shows that ruby was not designed as a functional programming language, and how by using Ruby’s dangerous flexibility you can manage to warp it towards your needs.

Personally, I have not used Symbol#to_proc because of performance concerns. But it appears that Rails now implements a more efficient version which is only a 20% performance hit for methods which yield only one argument. The way to both speed things up for methods that yield multiple arguments, and to avoid using the & syntax is to redefine the Enumerable methods to allow them to take a symbol as an argument. This is a method-by-method effort. Here is how it would be done for inject.

alias_method :__original_inject__, :inject

def inject meth_or_acc=nil, &block
  if not block_given? and meth_or_acc
    inject {|acc, n| acc.send meth_or_acc, n }
  else
    __original_inject__ meth_or_acc, &block
  end
end

After learning more languages, Ruby is starting to become pretty ugly to me. Functional languages put it to shame for examples like this. But somehow people keep managing to ignore all its faults, make little hacks, build declaritive frameworks, and be very productive with it.

© Greg Weber. All original code snippets are placed in the public domain.
Written on Mon Feb 25 18:04:26 2008.
Tagged as linux, ruby, script.
Send author an email.