Method References in Ruby
April 10th, 2014Today 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.