3

It seems that a double-splatted block parameter calls to_ary on an object that is passed, which does not happen with lambda parameters and method parameters. This was confirmed as follows.

First, I prepared an object obj on which a method to_ary is defined, which returns something other than an array (i.e., a string).

obj = Object.new
def obj.to_ary; "baz" end

Then, I passed this obj to various constructions that have a double splatted parameter:

instance_exec(obj){|**foo|}
# >> TypeError: can't convert Object to Array (Object#to_ary gives String)
->(**foo){}.call(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
def bar(**foo); end; bar(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)

As can be observed above, only code block tries to convert obj to an array by calling a (potential) to_ary method.

Why does a double-splatted parameter for a code block behave differently from those for a lambda expression or a method definition?

sawa
  • 165,429
  • 45
  • 277
  • 381

1 Answers1

8

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.

sawa
  • 165,429
  • 45
  • 277
  • 381
mrzasa
  • 22,895
  • 11
  • 56
  • 94