Ruby Symbols Instead of Blocks

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

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 block argument into a proc, allowing it to be called.

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

And 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. For example String#to_i converts strings into integers.

"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 prices.reduce(&:+) piece. A quick, simple, and shorthand way to express + without having to write out proc { |number| self + number }.

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 need the object to first be proc before it does anything. In other words, &object is just shorthand for 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

And when that proc is called

This means that :methods.to_proc will be the equivalent to the following.

def methods_to_proc(obj)
  obj.send(:methods)
end

This results in:

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

Now we're going to be 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 allowed 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_to because we used :introduce_to to create it on the first parameter. It is going to send any additional parameters as arguments.

With Map

Ok, lets get to the real world examples. We often see to_proc commonly used with an 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 do |obj, *args| 
  obj.send(:introduce, *args)
end

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 do |obj| 
  obj.send(:introduce)
end

Which just is sending the message introduce our object, the same as:

['ryan', 'steve', 'jill'].map do |obj| 
  obj.introduce
end

You can see that by expanding and reducing the &:symbol notation we can end up with a very familiar map and 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 argument to a block.

How about inject? Its block expects two parameters, result and element.

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

Lets do what we did above, expand the &:+ notation.

# 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
Ryan Toronto

Ember developer & Basketball fan