Stupid Ruby Tricks: String#to_proc
I was thinking about one of the cool bits of ActiveSupport, Symbol#to_proc, which allows you to do this:
[1, 2, 3].map(&:to_s) #=> ["1", "2", "3"]
That’s useful for sorting, enumerating, mapping, etc. Quite handy, and it saves you the trouble of writing a block to return a single value. I was recently someplace with a fair bit of time to kill and no reading material, and I began to wonder… how does this work?
Magic via one of the few typographic characters I can’t even write
Oh, the ampersand. Curly figure of import. That little pretzely freak is the sin qua non for this entire operation. You can tell, because Array#map acts all angry when you try to just pass it a Symbol:
irb(main):001:0> [1, 2, 3].map(:to_s)
ArgumentError: wrong number of arguments (1 for 0)
from (irb):1:in `map'
from (irb):1
The ampersand is how Ruby denotes a block variable.
Let’s try to pass our block in a different way:
my_block = lamdba{ |x| x.to_s }
[1, 2, 3].map(my_block)
Ruby will raise an ArgumentError because it’s trying to pass your block as a regular method parameter, rather than as a block parameter. Let’s try passing it as a block parameter:
[1, 2, 3].map(&my_block) #=> ["1", "2", "3"]
Sweet! It worked!
Now we’ve got a better of idea of what the hell we’re actually doing. When we write map(&:to_s), we’re saying “call the map function and pass it this block.” But a Symbol isn’t a block!
Very true, and what does Ruby try to do when presented with a type mismatch? Coerce! How? Conversion methods! Anything with a to_proc method can be converted to a proc. Our first example can be rewritten as follows:
[1, 2, 3].map(&:to_s.to_proc) #=> ["1", "2", "3"]
This means that anything with a to_proc method can partake of this lovely syntactic sugar.
Pushing the limits for no apparent reason
And now for the inevitable section of any article on programming that I write: taking things too far.
class String
def to_proc
eval "Proc.new { |*args| args.first#{self} }"
end
end
robots = []
robots << { :name => 'Jeevesotron', :type => 'robo-butler' }
robots << { :name => 'Crushmonster', :type => 'robo-baby-sitter' }
robots << { :name => 'Fluffy', :type => 'doombot' }
robots.map(&'[:name].upcase') #=> ["JEEVESOTRON", "CRUSHMONSTER", "FLUFFY"]
It may not be useful, but it sure as hell is fun.
August 1st, 2006 at 11:47pm
Questions:
1. What’s the difference between a proc and a block and why can’t Ruby coerce a proc to a block for us?
2. I don’t understand what args.first#{self} is doing. Can you explain?
August 2nd, 2006 at 12:20am
A block is a proc in the right place. I know that seems odd, but “block” is a very vague term, whereas proc is precise (an instance of the
Procclass). A block is the chunk of code attached to the end of a method; for example in this snippetthe
{ |x| x.upcase }bit is the block. Ruby stores that executable code into aProcinstance, which is then accessible by the method (either implicitly, viayield, or explicitly, by declaring a block variable as the last parameter of the method (e.g.,def blah(something, &block))).Does that make sense?
Your last question is easier. That chunk is replaced by the string’s contents, so that given a string of
".property.value", the dynamically createdProcwould return the evaluation of"args.first.property.value".August 2nd, 2006 at 1:06am
Very sweet. You are right, it may not be useful per se, but very helpful to understand some nuances of ruby, and just for that, it worth it!
Thank you, Coda!
August 2nd, 2006 at 7:21pm
Thanks, nicely explained. The variable substitution I understood but I guess I was a bit confused by which arguments args actually refered to and how self was being used. From what I can the robot is the first argument to the proc/block and self refers to the string that is getting type converted. A little confusing at first but basically sane.
August 6th, 2006 at 8:24am
Actually, that last example *was* useful. I’ve had several instances where I wish that would have worked. Though, of course, as I write this, I can’t think of them.
Though, I know they followed the “but I want to do *two* methods on each one!” style that you presented on your example.
Can you add this to our lib?
-h.
August 16th, 2006 at 12:29pm
Useful indeed, and fun like you said. A good explanation of the block/proc nuance of ruby in the comments here too.