Ruby symbols instead of blocks

by Ryan on August 6, 2011

Every wonder why this works? It’s straight forward and easy to read, but how does it work internally in Ruby?

[1, 2, 3].inject(&:+)
# => 6

Meet &

The & operator in Ruby lets us go from Proc to block and vise-versa, but only in certain places.

In fact, there are two two places where we can use &

In a method definition &blk will turn the blk argument into a proc, allowing it to be called.

def block_to_proc &blk
  blk.call
end
block_to_proc { "hello" }
# => "hello"

In a method call it coverts the argument into a block.

def proc_to_block
  yield
end
proc_to_block(&proc { "hello" })
# => "hello"

To Proc

Ruby has a lot of to_X methods. They are designed to express their receiver as another type.

"1".to_i + 2
# => 3

Well, to_proc exists too. Expressing non proc objects as procs? When and why would we ever do that? An example is our [...].inject(:+) piece. A quick, simple, shorthand way to express a block without having to write it all out.

Anytime we are trying to express an object as a proc Ruby will check to see if that object responds to to_proc. If it does it will use the return value to express the object as a proc. Since &object can covert procs to blocks the & is going to want the object to be a Proc before it does anything. So &object is going to really be &object.to_proc.

Symbol’s To Proc

Symbol’s to_proc is what allows us to pass it in place of a block. It might look a little strange at first, but once you see it used it becomes pretty cool.

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

This returns a proc that takes 2 parameters

  • First is an object that will receive the method.
  • The second is the arguments that will be passed into the method.

And when that proc is called

  • Sends the original symbol to the object with the arguments.

So, :methods.to_proc basically builds this method.

# :methods.to_proc
def methods_to_proc_when_called obj, *args
  obj.send(:methods, *args)
end

Lets play with :symbol.to_proc in some funny ways.

my_little_proc = :methods.to_proc
# my little proc is ready to call :methods on any object
 
my_little_proc.call(String)
# => returns an array all of String's methods

The Setup

We’re basically creating a proc from a symbol that can be used to send that symbol into any object. In fact, a more fitting name for my_little_proc would be something like call_my_symbol_on.

class String
  def introduce
    puts "Hi I'm #{self}"
  end
 
  def introduce_to name
    puts "Hi #{name}, I'm {self}"
  end
end
 
call_my_symbol_on = :introduce.to_proc
call_my_symbol_on("ryan")
# => "Hi I'm ryan"

Since the proc that to_proc returned is setup to take arguments.

call_my_symbol_on = :introduce_to.to_proc
call_my_symbol_on("ryan", "steve")
# => "Hi steve, I'm ryan"

If you think about it, this is really simple. call_my_symbol_on is just going to call introduce (because we used :introduce to create it) on the first parameter. It is going to send the 2nd+ parameters in as arguments.

With Map

Ok, lets get to the real world examples. We often see to_proc commonly used with enumerable. Lets say we want to introduce a bunch of names.

['ryan', 'steve', 'jill'].map(&:introduce)
# => Hi I'm ryan
# => Hi I'm steve
# => Hi I'm jill

Is really

['ryan', 'steve', 'jill'].map(&:introduce.to_proc)

Which can be expressed as

['ryan', 'steve', 'jill'].map( & proc{ |obj, *args| obj.send(:introduce, *args) } )

And & is now going to covert that proc into a block. That line can now become

['ryan', 'steve', 'jill'].map{ |obj, *args| obj.send(:introduce, *args) }

And since map only passes one argument, the element, to its block there is really no need to express the additional arguments.

['ryan', 'steve', 'jill'].map{ |obj| obj.send(:introduce) }

Which is sending the message introduce our object, which we know is the same as

['ryan', 'steve', 'jill'].map{ |obj| obj.introduce }

You can see that by expanding and reducing the &:symbol notation we can end up with a very familiar call map with block.

Now with some *Args

So what about *args? We were able to drop it, because map only expects a block that will yield to one element. We need to find a common ruby method that yields more than one thing to a block.

How about inject? It’s block expects 2 parameters, result and element.

Enumerable.inject(start) { |result, element| … }

Lets do what we did above. But this time get from

# turn
(1..10).inject(&:+)
 
# into
(1..10).inject do |result, element| 
  result + element
end

Here we go

(1..10).inject(&:+)
(1..10).inject(&:+.to_proc)
 
# can be expressed as
 
(1..10).inject( &proc{ |obj, *args| obj.send(:+, *args) } ) 
 
# which & will covert into
 
(1..10).inject( |obj, *args| obj.send(:+, *args) } ) 
 
# which we can convert to
 
(1..10).inject do |obj, *args| 
  obj.send(:+, *args) 
end
 
# and then we can just rename our parameters to something more friendly
 
(1..10).inject do |result, element| 
  result.send(:+, element) 
end
 
# => 55
One Response leave one →
  1. August 9, 2011

    There’s also

    [5, 7, 8, 1].each(&method(:puts))

    which does puts(number) on each number.

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS