I don't have full answers to your questions, but I'll share what I've found out.
Short version
Procs allow to be called with number of arguments different than defined in the signature. If the argument list doesn't match the definition, #to_ary
is called to make implicit conversion. Lambdas and methods require number of args matching their signature. No conversions are performed and that's why #to_ary
is not called.
Long version
What you describe is a difference between handling params by lambdas (and methods) and procs (and blocks). Take a look at this example:
obj = Object.new
def obj.to_ary; "baz" end
lambda{|**foo| print foo}.call(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
proc{|**foo| print foo}.call(obj)
# >> TypeError: can't convert Object to Array (Object#to_ary gives String)
Proc
doesn't require the same number of args as it defines, and #to_ary
is called (as you probably know):
For procs created using lambda
or ->()
, an error is generated if wrong number of parameters are passed to the proc. For procs created using Proc.new
or Kernel.proc
, extra parameters are silently discarded and missing parameters are set to nil
. (Docs)
What is more, Proc
adjusts passed arguments to fit the signature:
proc{|head, *tail| print head; print tail}.call([1,2,3])
# >> 1[2, 3]=> nil
Sources: makandra, SO question.
#to_ary
is used for this adjustment (and it's reasonable, as #to_ary
is for implicit conversions):
obj2 = Class.new{def to_ary; [1,2,3]; end}.new
proc{|head, *tail| print head; print tail}.call(obj2)
# >> 1[2, 3]=> nil
It's described in detail in a ruby tracker.
You can see that [1,2,3]
was split to head=1
and tail=[2,3]
. It's the same behaviour as in multi assignment:
head, *tail = [1, 2, 3]
# => [1, 2, 3]
tail
# => [2, 3]
As you have noticed, #to_ary
is also called when when a proc has double-splatted keyword args:
proc{|head, **tail| print head; print tail}.call(obj2)
# >> 1{}=> nil
proc{|**tail| print tail}.call(obj2)
# >> {}=> nil
In the first case, an array of [1, 2, 3]
returned by obj2.to_ary
was split to head=1
and empty tail, as **tail
wasn't able to match an array of[2, 3]
.
Lambdas and methods don't have this behaviour. They require strict number of params. There is no implicit conversion, so #to_ary
is not called.
I think that this difference is implemented in these two lines of the Ruby soruce:
opt_pc = vm_yield_setup_args(ec, iseq, argc, sp, passed_block_handler,
(is_lambda ? arg_setup_method : arg_setup_block));
and in this function. I guess #to_ary
is called somewhere in vm_callee_setup_block_arg_arg0_splat, most probably in RARRAY_AREF
. I would love to read a commentary of this code to understand what happens inside.