Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 452
Ze Gao Avatar asked Sep 14 '25 07:09

Ze Gao


1 Answers

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)
like image 168
Jörg W Mittag Avatar answered Sep 15 '25 23:09

Jörg W Mittag