Ruby Closure Surprises

Doing some fun stuff with named_scope this morning led me to some surprising discoveries in Ruby's handling of blocks/closures/etc.

In Ruby, I had thought that lambda was supposed to do arity enforcement and that Proc.new wasn't (and also that return behaves differently in Proc's vs lambda's) and that this was the extent of the differences. See here.

However, I was getting a "warning: multiple values for a block parameter (0 for 1)" warning when doing this:

  named_scope :ordered, Proc.new {|o| {:order => o || "created_at DESC"}}

and I then also discovered that the following also worked (with the same warning) whereas I thought it would have raised an exception because it was lambda:

  named_scope :ordered, lambda {|o| {:order => o || "created_at DESC"}}

My coworker Jeff found this fascinating and revealing investigation into Ruby closures where he reveals "four types of closures and near-closures, expressible in seven syntactic variants." Yikes.

However, I think the chart he has about half way down is either outdated or a little wrong. Here's what I came up with after a little investigation of my own. Perhaps I'll write down a test for these at some point to test future Ruby versions (and to make sure I got the chart right). But for now --

                                                    "return" returns from closure
                                   True closure?    or declaring context...?         Mismatched arity behavior
                                   ---------------  -----------------------------    -------------------
1. block (called with yield)       N                declaring                        Behavior 1
2. block (&b => f(&b) => yield)    N                declaring                        Behavior 1
3. block (&b => b.call)            Y except return  declaring                        Behavior 1
4. Proc.new                        Y except return  declaring                        Behavior 1
5. proc                                    <<< alias for lambda in 1.8, Proc.new in 1.9 >>>
6. lambda                          Y                closure                          Behavior 3
7. method                          Y                closure                          Behavior 2

* Behavior 1: Succeed (supply nil for missing args and ignore extra args).  Exception: When block is defined to accept exactly 1 parameter then warn on any mismatch
* Behavior 2: raise ArgumentError on mismatch 
* Behavior 3: same as Behavior 2 except for the following:
    - Same as Behavior 1 if block has exactly one parameter, e.g. -- lambda{|a| puts a}.call # warn and succeed
    - Same as Behavior 1 if block was defined w/o argument pipes, e.g. -- lambda{puts 'hi'}.call(1,2) # succeed

It's a crazy Ruby world :)