codahale.com٭blog

This is my old blog. My current writing is here: codahale.com

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 Responses to “Stupid Ruby Tricks: String#to_proc”

  1. Glen Stampoultzis Says:

    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?

  2. Coda Says:

    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 Proc class). A block is the chunk of code attached to the end of a method; for example in this snippet

    blah.map{ |x| x.upcase }

    the { |x| x.upcase } bit is the block. Ruby stores that executable code into a Proc instance, which is then accessible by the method (either implicitly, via yield, 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 created Proc would return the evaluation of "args.first.property.value".

  3. Pascal Says:

    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!

  4. Glen Stampoultzis Says:

    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.

  5. Hampton Says:

    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.

  6. Benjamin Says:

    Useful indeed, and fun like you said. A good explanation of the block/proc nuance of ruby in the comments here too.