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.
6 comments »