Most Rubyists would know what the following does.
[1, 2, 3].map(&:to_s)
It takes the array of numbers, calls to_s on each item, and returns an array of the return values.
But what if you want to do the opposite? Rather than calling a method on the object, what if you want to call a method with the object as a parameter? Simple!
["1", "2", "3"].map(&method(:Integer))
Wait … what?
I’m sure you all understand how it works, because you already use
map(&:to_s), and none of you would use something you don’t understand, right?
Who am I kidding?
I’ll explain how
map(&:method_name) works, and then how
When you use & (ampersand) in front of the last variable in a method call, you’re saying that what’s passed in shouldn’t be treated as an object, but treated like a piece of code – a proc.
Symbols aren’t procs, but they can be converted into them by Ruby. You can see for yourself by running
call on it with the following:
# my_proc will be something like #<Proc:0x00000001014c39c8@(irb):35>
my_proc = :reverse.to_proc
# This causes "abc".reverse to be called, which returns "cba"
As Ruby calls `
to_proc` if an ampersand parameter isn’t a proc, the following
will print the string reversed three times.
Now that the
& part is understood, let’s look at the
When you call
method(:method_name), you get back an object of class Method that represents the method.
# Integer(string) is a method kind of like String#to_i, but 100% more awesome.
# Look it up if you haven't done so already.
# PS: don't worry about it starting with a capital letter!
# another_proc will look like #<Method: Object(Kernel)#Integer>
another_proc = method(:Integer)
another_proc.call("42") # Returns 42
and as `
Method#to_proc` exists, the `
call_me_three_times` method will convert the method object into a proc and will run it without any complaints.
When you run `
call_me_three_times("42", &another_proc)`, it’ll print 42 (not “42”) three times.
But wait … there’s more!
The method doesn’t even have to be from your own object – it can be something else’s. For example, if you wanted to read in a series of files, you could do
filenames = ["a.txt", "b.txt", "c.txt"]
texts = filenames.map(&File.method(:read))
File.method(:read) turns the read singleton method of the File class into an object, and then (with the help of
Method#to_proc) that object gets passed to map as something that should be executed on each filename.
Firstly, you may need to document what &method does. I’ve been programming in Ruby for over three years, and hadn’t come across this trick until I noticed it on Stack Overflow. However, it is mentioned in the Pickaxe, now that I looked it up.
Second, there may be performance penalties for this approach. If hardly anybody has used this approach, it’s unlikely to have been optimized.
Third, making code shorter isn’t always a good thing. It may make smelly code seem less smelly. If you’re doing twenty different things on one line, the answer isn’t to cram things up to make the line shorter, but to refactor that line into something more logical.
Fourth, I haven’t worked out how to abuse this trick fully. Under Ruby 1.9, if foo takes two arguments, I don’t know how to do
[["a", "b"], ["c", "d"]].map(&method(:foo)) yet. That code works under 1.8, however.
(Apologies for the awful appearance of the code – I haven’t worked out how to make wordpress use Github gists without adding a plugin)