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 SymbolI 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.
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
class SymbolModifying the buggy symbol to proc implementation a bit, we get more insight into the problem.
def to_proc
Proc.new { |*args| args.shift.__send__(self, *args) }
end
end
class SymbolNotice 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.
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
this should shed some light
ReplyDeleteI feel really dumb, but I didnt understand that :/
ReplyDeleteI've annotated the pastie for clarity; see if it makes sense now.
ReplyDeletehttp://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. :|
"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.
ReplyDeleteI'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?