Monday, September 7, 2009

Buggy Symbol to Proc Implementation

     Ruby 1.9 seems to have introduced symbol to proc into the core language itself. I'm not sure whether this is implemented natively or as Ruby code, so if anyone knows that, please do let me know. For people who don't know what symbol to proc is (which is, I suspect, a very small part of the ruby community), click here

     Recently I was doing something on irb 1.8 and I missed having symbol to proc around. Rather than require ActiveSupport, the gem which includes this enhancement to symbol, I just googled for the code and found this, which was the first hit in google when I searched for "symbol to proc". I pasted the code into irb and continued ... and then I suddenly faced a strange error.

class Symbol
def to_proc
Proc.new { |obj, *args| obj.send(self, *args) }
end
end
>> [[1],[-3,4]].map(&:size)
ArgumentError: wrong number of arguments (1 for 0)
from (irb):58:in `size'
from (irb):58:in `send'
from (irb):58:in `to_proc'
from (irb):71:in `map'
from (irb):71
     I was quite mystified by this error. Now this does not happen when you use the symbol to proc implementation currently in Rails (listed below), or the Ruby 1.9 implementation.

class Symbol
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
end
     Modifying the buggy symbol to proc implementation a bit, we get more insight into the problem.

class Symbol
def to_proc
Proc.new { |obj, *args| puts "obj is #{obj}"; puts "args has #{args.size} elements"; obj.send(self, *args) }
end
end
>> [1,-3,4].map(&:abs)
obj is 1
args has 0 elements
obj is -3
args has 0 elements
obj is 4
args has 0 elements
=> [1, 3, 4]
>> [[1],[-3,4]].map(&:size)
obj is 1
args has 0 elements
obj is -3
args has 1 elements
ArgumentError: wrong number of arguments (1 for 0)
from (irb):58:in `size'
from (irb):58:in `send'
from (irb):58:in `to_proc'
from (irb):71:in `map'
from (irb):71
      Notice how in the second case, obj seems to be not the array, but the head of the array. And the remaining elements have become arguments. I'm not sure if the symbol to proc implementation on that page was always wrong, or if ActiveSupport once had a buggy implementation that was subsequently fixed. One thing I've been unable to do is to write some sample code to recreate the problem without using symbol to proc at all. Just writing a couple of functions with varargs and invoking them with array arguments. If anyone is able to construct such an example, that would be great. Here is an additional issue with symbol to proc (performance related). And here is a workaround.

4 comments:

  1. I feel really dumb, but I didnt understand that :/

    ReplyDelete
  2. I've annotated the pastie for clarity; see if it makes sense now.

    http://pastie.org/612121

    Long story short, 'cause you're giving it a block of arity 2 (anything more than 1), it's unrolling the array; but activesupport is collecting the argument(s) (in your case the array passed in) into a wrapper array with the * notation.

    Ok, so that wasn't very short, but rest assured it can be longer!

    One more reason to hate ruby. :|

    ReplyDelete
  3. "Long story short, 'cause you're giving it a block of arity 2 (anything more than 1), it's unrolling the array" <--- yeah thats the surprising bit for me. Why does that happen? Everything else was as I expected, but this was definitely unexpected.

    I've created a new pastie just demonstrating that bit. And now it all makes sense.

    http://pastie.org/612252

    Just changing the arity of the block being passed, changed what was yielded. Any idea why this behavior is good? or considered unsurprising?

    ReplyDelete