Method References in Ruby

April 10th, 2014

Today I was writing some Ruby code for filtering arrays when I recalled that Ruby has a shorthand notation to pass a method by its name (as a symbol). I’m writing this post to help me remember, while I figure out how it really works.

Say, for example, that we need to select the odd numbers from a sequence. Of course we could pass a block to do that.

(1..9).select { |n| n.odd? } #=> [1, 3, 5, 7, 9]

But we can also pass a symbol, prefixed with an ampersand. While not actually accurate (in a few minutes you’ll see why), this is what I call a method reference.

(1..9).select &:odd? #=> [1, 3, 5, 7, 9]

Personally I prefer the latter form as it’s a better representation of the essence that I want my code to do: from range 1 to 9 select only odd numbers.

How does it work?

When Googling for information on the ampersand unary operator in Ruby, I first found a few blog posts and Stack Overflow answers that told me that & calls to_proc on a given object. That would be convenient, although it implies that this would probably only work for methods that accept both a block or a proc. Let’s worry about that later.

First try and see if this works. If & would call to_proc on the symbol I should be able to pass a proc myself, too.

(1..9).select :foo?.to_proc
ArgumentError: wrong number of arguments (1 for 0)

Whoops! That didn’t work too well. What’s even more interesting is that if I just put the ampersand in front, the code runs.

(1..9).select &:foo?.to_proc #=> [1, 3, 5, 7, 9]

So maybe these first hits weren’t actually correct about things. At this point it seems more likely that ampersand turns a proc into a block. And given any object, it will get a proc by calling to_proc first.

To see how my new theory works out I use a custom object that answers to to_proc. Because Ruby’s blocks are special things that cannot be created or assigned to variables, I cannot simply capture the resulting value of &:foo. Let’s give it a try.

odds = Object.new
def odds.to_proc() ->(n) { n.odd? } end

(1..9).select &odds #=> [1, 3, 5, 7, 9]

Awesome! However, this doesn’t really confirm that the ampersand in fact turns the proc into block, only that actually to_proc is called on the object.

To check that the ampersand turns the proc into a block I use a function that yields.

def yielder(n) yield n end

yielder 1, &odds #=> true
yielder 2, &odds #=> false

While passing a proc obviously won’t work.

yielder 1, odds.to_proc
ArgumentError: wrong number of arguments (2 for 1)

Tentatively my conclusion is that the ampersand unary operator takes a duck-type of a proc (i.e. an object that responds to to_proc) and returns a block. If you see any flaws in this, don’t hesitate to let me know.