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)
October 3, 2011 at 10:49 pm |
Hm. It was interesting to Read
January 11, 2012 at 3:03 pm |
Yup, good read. Post more ruby tips 😉
April 15, 2012 at 1:19 am |
Hi Avdi,
I happened to read both this post and a draft of your “Object on Rails” book. Do you think &method provides a good tool for left-to-right “decoration” of data and addition of behaviour?
This gist kinda exemplifies my point: https://gist.github.com/2381626
If placing readability of flow of data, then
originalData.pat(&method(:FooType)).pat(&method(:BarType)).message
is, in a way, better than
BarType.new(FooType.new(originalData)).message
What do you think?
April 15, 2012 at 10:28 am |
Hi Serguei. I am a Grimm, I am an A. grimm, but I’m not Avdi Grimm! 🙂
April 15, 2012 at 4:56 pm |
Oh damn, sorry Andrew. I’m still interested in what YOU think about the &method being used to write flow of data left-to-right through decorations.
April 18, 2012 at 11:19 am |
I don’t know much at all about design patterns and the like in Ruby. I think me answering would make the internet a worse place. 🙂
July 29, 2014 at 10:15 am |
really nice article. Thanks for sharing. Keep it up