« Enterprise Integration with Ruby goes Beta | Main | The First Rails Studio »

November 03, 2005

Symbol#to_proc

The Ruby Extensions Project contains an absolutely wonderful hack. Say you want to convert an array of strings to uppercase. You could write
  result = names.map {|name| name.upcase}

Fairly concise, right? Return a new array where each element is the corresponding element in the original, converted to uppercase. But if you include the Symbol extension from the Ruby Extensions Project, you could instead write

  result = names.map(&:upcase)

Now that’s concise: apply the upcase method to each element of names. So, how does it work?

It relies on Ruby doing some dynamic type conversion. Let’s start at the top.

When you say names.map(&xxx), you’re telling Ruby to pass the Proc object in xxx to map as a block. If xxx isn’t already a Proc object, Ruby tries to coerce it into one by sending it a to_proc message.

Now :upcase isn’t a Proc object—it’s a symbol. So when Ruby sees names.map(&:upcase), the first thing it does is try to convert the Symbol :upcase into a Proc by calling to_proc. And, by an incredible coincidence, the extension project has defined a to_proc method for class Symbol. It looks like this:

    def to_proc
      proc { |obj, *args| obj.send(self, *args) }
    end

It creates a Proc which, when called on an object, sends that object the symbol itself. So, when names.map(&:upcase) starts to iterate over the strings in names, it’ll call the block, passing in the first name and invoking its upcase method.

It’s an incredibly elegant use of coercion and of closures.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/t/trackback/2226312/7670454

Listed below are links to weblogs that reference Symbol#to_proc:

Comments

Nice hack. But there's a case in that it does not behave as expected.

I got surprised when handling arrays. Simple example (using Ruby/Extensions):
[[1,2], [3,4,5]].map(&:size)
I expected [2, 3] but it resulted in an ArgumentError, because of trying to call "1.send(:size,2)" and "3.send(:size,[3,4,5])" instead of "[1,2].send(:size)" and "[3,4,5].send(:size)".

This happens because of Ruby's way assigning arrays to multiple variables. A small change makes the hack more robust:

class Symbol
def to_proc
proc { |*args| args[0].send(self, *args[1...args.size]) }
end
end

I considered these three test cases:

%w{john terry fiona}.map(&:capitalize)
[5,6,2].inject(&:+)
[[0,1], [2,3,4]].map(&:size)

All three work as expected with my version. The Ruby/Extensions version on the other hand screws up the last one. And it's easy to make a (too) simple to_proc which screws up the second one.

This is very cool looking, but it is slow as hell.

http://www.ruby-forum.com/topic/161089

Hey dave,

thanks to that article I finally understood the *args multiple parameters thingy. Thanks for that!

Besides, I found that creating a Proc object for each invocation may cause a serious memory and runtime penalty. I thought about caching the Proc inside the Symbol, i.e.

def to_proc
@to_proc ||= Proc.new { |*args| args.shift.__send__(self, *args) }
end

As far as I can see this should work just fine. But if a Proc bears some invisible luggage, like a thread environment or such, this would fail, of course. So, given your ruby experience: Would you say that approach is safe?

I have a post on my blog that goes into that in more detail: http://1rad.wordpress.com/2008/11/10/0x0a-some-optimization-hacks/

Post a comment

If you have a TypeKey or TypePad account, please Sign In

Now in Beta

  • Programming Ruby, 3rd Edition
    Third Edition, Covering Ruby 1.9, now in beta
My Photo

Pragmatic Stuff

Photos

  • www.flickr.com
    This is a Flickr badge showing public photos from pragdave tagged with pragdave_badge. Make your own badge here.

Site Search

  • Google Search

    The web
    PragDave