Ze Gao
Ze Gao

Reputation: 91

How does ruby unpack arguments passed into Proc?

a_proc = Proc.new {|a,b,*c| p c; c.collect {|i| i*b }}
puts a_proc[2,2,4,3]

Code above is pretty intuitive according to https://ruby-doc.org/core-2.2.0/Proc.html, a_proc[2,2,4,3] is just a syntax sugar for a_proc.call(2,2,4,3) to hide “call”

But the following (works well) confused me a lot

a=[2,2,4,3]
puts a_proc.call(a)
puts a_proc.call(*a)

It seems very different from a normal function call, cause it doesn't check the number arguments passed in.

However, as expected the method calling semantics will raise an error if using parameters likewise

def foo(a,b,*c)
  c.collect{|i| i*b}
end
foo([1,2,3,4]) #`block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1,2,3,4]) #works as expected

I do not think such an inconsistency as a design glitch, so any insights on this will be appreciated.

Upvotes: 4

Views: 2307

Answers (2)

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369468

Blocks use different semantics than methods for binding arguments to parameters.

Block semantics are more similar to assignment semantics than to method semantics in this regard. In fact, in older versions of Ruby, blocks literally used assignment for parameter binding, you could write something like this:

class Foo; def bar=(val) puts 'setter called!' end end

some_proc = Proc.new {|$foo, @foo, foo.bar|}
some_proc.call(1, 2, 3)
# setter called!
$foo #=> 1
@foo #=> 2

Thankfully, this is no longer the case since Ruby 1.9. However, some semantics have been retained:

  • If a block has multiple parameters but receives only a single argument, the argument will be sent a to_ary message (if it isn't an Array already) and the parameters will be bound to the elements of the Array
  • If a block receives more arguments than it has parameters, it ignores the extra arguments
  • If a block receives fewer arguments than it has parameters, the extra parameters are bound to nil

Note: #1 is what makes Hash#each work so beautifully, otherwise, you'd always have to deconstruct the array that it passes to the block.

In short, block parameters are bound much the same way as with multiple assignment. You can imagine assignment without setters, indexers, globals, instance variables, and class variables, only local variables, and that is pretty much how parameter binding for blocks work: copy&paste the parameter list from the block, copy&paste the argument list from the yield, put an = sign in between and you get the idea.

Now, you aren't actually talking about a block, though, you are talking about a Proc. For that, you need to know something important: there are two kinds of Procs, which unfortunately are implemented using the same class. (IMO, they should have been two different classes.) One kind is called a lambda and the other kind is usually called a proc (confusingly, since both are Procs).

Procs behave like blocks, both when it comes to parameter binding and argument passing (i.e. the afore-described assignment semantics) and also when it comes to the behavior of return (it returns from the closest lexically enclosing method).

Lambdas behave like methods, both when it comes to parameter binding and argument passing (i.e. strict argument checking) and also when it comes to the behavior of return (it returns from the lambda itself).

A simple mnemonic: "block" and "proc" rhyme, "method" and "lambda" are both Greek.


A small remark to your question:

a_proc[2,2,4,3] is just a syntax sugar for a_proc.call(2,2,4,3) to hide “call”

This is not syntactic sugar. Rather, Proc simply defines the [] method to behave identically to call.

What is syntactic sugar is this:

a_proc.(2, 2, 4, 3)

Every occurrence of

foo.(bar, baz)

gets interpreted as

foo.call(bar, baz)

Upvotes: 8

Dbz
Dbz

Reputation: 2761

I believe what might be confusing you are some of the properties of Procs. If they are given a single array argument, they will automatically splat it. Also, ruby blocks in general have some interesting ways of handling block arguments. The behavior you're expecting is what you will get with a Lambda. I suggest reading Proc.lambda? documentation and be careful when calling a ruby block with an array.

Now, let's start with the splat operator and then move to how ruby handles block arguments:

def foo(a, b, *c) 
  c.map { |i| i * b } # Prefer to use map alias over collect
end

foo([1, 2, 3, 4]) # `block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1, 2, 3, 4]) # works as expected

So in your argument error, it makes sense: def foo() takes at least two arguments: a, b, and however many with *c. The * is the splat operator. It will turn an array into individual arguments, or in the reverse case here, a variable amount of arguments into an array. So when you say foo([1,2,3,4]), you are giving foo one argument, a, and it is [1,2,3,4]. You are not setting b or *c. What would work is foo(1, 1, 1, 2, 3, 4]) for example because you are setting a, b, and c. This would be the same thing: foo(1, 1, *[1,2,3,4]).

Now foo(*[1, 2, 3, 4]) works as expected because the splat operator (*) is turning that into foo(1, 2, 3, 4) or equivalently foo(1, 2, *[3, 4])

Okay, so now that we have the splat operator covered, let's look back at the following code (I made some minor changes):

a_proc = Proc.new { |a, b, *c| c.map { |i| i * b }}
a = [1, 2, 3, 4]
puts a_proc.call(a)
puts a_proc.call(*a)

Remember that if blocks/procs are given a single array argument they will automatically splat it. So if you have an array of arrays arrays = [[1, 1], [2, 2], [3, 3]] and you do arrays.each { |a, b| puts "#{a}:#{b}" } you are going to get 1:1, 2:2, and 3:3 as the output. As each element is passed as the argument to the block, it sees that it is an array and splats it, assigning the elements to as many of the given block variables as it can. Instead of just putting that array in a such as a = [1, 1]; b = nil, you get a = 1; b = 1. It's doing the same thing with the proc.

a_proc.call([1, 2, 3, 4]) is turned into Proc.new { |1, 2, [3, 4]| c.map { |i| i * b }} and will output [6, 8]. It splits up the arguments automatically it's own.

Upvotes: 2

Related Questions