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 :)
