Archive for the ‘tips’ Category

In Ruby, &method passes you!

October 3, 2011

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 map(&method(:method_name)) works.

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 to_proc and 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"
my_proc.call("abc")

As Ruby calls `to_proc` if an ampersand parameter isn’t a proc, the following

def call_me_three_times(parameter_1)
  puts((yield parameter_1).inspect)
  puts((yield parameter_1).inspect)
  puts((yield parameter_1).inspect)
end

call_me_three_times("abc", &:reverse)

will print the string reversed three times.

Now that the & part is understood, let’s look at the method part.

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.

Downsides

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)

Advertisements