Some consistency, please?

So I posted a few days ago about Ruby 1.9's Symbol#to_proc addition. That cute little bit of source code that allows us to write:

burgers.map(&:sauce)

Pretty cool, eh? Only it turns out there's a problem. But first why don't we talk a little bit about Procs and Lambdas in Ruby?

I mention this briefly in my article If It's Not Nailed Down, Steal It, but there are some differences between them.

Of course, it's not really accurate to talk about "Lambdas." Lambda expressions still return instances of the Proc class.

lambda{|x| x }.class.name ===> Proc

But Procs created with "Proc.new" and "lambda" do behave differently. First of all, a "return" statement inside a lambda-created Proc will return control from the block. A "return" statement inside a Proc.new-created Proc will return from the method where the Proc was defined.

This difference is weird, but fairly utilitarian. After all, it's fairly common to use an each iterator statement inside a method and want to terminate both the iteration and the method early with a return statement. If return only exited the Proc/block, you wouldn't have a good way to short circuit out. Of course as it as, it's not really a general solution for arbitrary levels of nesting, but that's a sort of larger problem (perhaps solved by naming your blocks? hmmm...)

Anyways, but then it's also nice to have lambdas behave like methods for those situations when you need that kind of behavior. So for better or worse, that's one difference.

So, what else is different and what does this have to do with the Symbol#to_proc problem I alluded to earlier? Well, have a look at this code and its output:

require 'pp'
lambda{|a, *b| pp [a, b]}.call([1, 2, 3])
  ==> [[1, 2, 3], []]
Proc.new{|a, *b| pp [a, b]}.call([1, 2, 3])
  ==> [1, [2, 3]]

Yikes! What's going on here?

Well, it turns out that Procs and lambdas also have different parameter destructuring rules. I talk about destructuring in If It's Not Nailed Down..., but the short version is that it's a way to automatically unpack elements out of a data structure. We do this all the time in Ruby with:

a, b = [1, 2]
a
 ==> 1
b
 ==> 2

You see, Procs/blocks automatically unpack their arguments in some situations. You may have even used this to your benefit. Anytime you're working with groups of elements (pairs, for example), it's nice to be flexible about whether the person you're passing the group to wants the group or the separate items. So these two lines are the same:

Proc.new{|a, b| pp [a, b] }.call(1, 2)
 ==> [1, 2]
Proc.new{|a, b| pp [a, b] }.call([1, 2])
 ==> [1, 2]

So, in conclusion, by using Proc.new in the definition of Symbol#to_proc, some situations were introduced where the loosy-goosey handling of parameters in Procs/blocks caused problems. The problem was easily fixed, however, by replace Proc.new with lambda.

Update: Oops. In all my excitement over the dfferences between Proc.new and lambda, I complete forgot that the problem was actually fixed by forcing an array glom of the arguments and using the first one as invocant. This makes the final implementation:

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

Maybe there are some other differences between Proc and Lambda that make this approach more suitable? Anyways, pretty cool. Read Rodney's post for more info.

End update

The one other final change that ended up happening was to use .__send__ instead of .send because some classes (like Socket) redefine send. Doh. Anyways, good they fixed it, but shame on Socket (although you can understand why they'd want to use the method name send).

A very informative post by Rodney of pinupgeek.com put this on my radar, and you can read more on the Ruby Talk mailing list here and here.

posted on: 06/20/2006 | path: /tech