4
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.

Ze Gao
  • 91
  • 1
  • 7
  • Not sure design-wise why things are the way they are or how ruby is doing what with proc arguments, but check out the documentation for [`lambda?`](https://ruby-doc.org/core-2.2.0/Proc.html#method-i-lambda-3F) which explains some of the tricks that procs have (extra arguments are ignored, missing arguments are `nil`, arrays are expanded, etc) – Simple Lime Jul 29 '17 at 10:58
  • 3
    Thank you for asking an interesting question. This is a nice departure from the usual on SO. – Wayne Conrad Jul 29 '17 at 13:23
  • Change `p c` to `puts "a=#{a}, b=#{b}, c=#{c]"` and call the proc with a range of arguments. Then change the proc's arguments to, say, `|a,*b,c|` (and change the body accordingly), then again call the proc with different arguments, and so on. In no time at all you will be able to answer your own question. As a rule, if you, the human, see no logical and unique way to assign the block variables (e.g., `call(1,2,3,4,5)` with `|*a,b,*c|`), neither will Ruby, so she'll raise an exception; if you see one and only one logical assignment, Ruby will concur. – Cary Swoveland Jul 29 '17 at 21:02

2 Answers2

8

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)
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • 2
    Can you give an example of this `If a block receives multiple arguments but only has a single parameter, the parameter will be bound to an array of all arguments`? If you have `a_proc = proc { |a| a }` and pass it `a_proc.call(1, 2, 3)` the return value will be `1`, not `[1, 2, 3]` – m. simon borg Jul 29 '17 at 13:11
  • 1
    I misremembered. Fixed now. – Jörg W Mittag Jul 29 '17 at 18:08
  • 1
    Jörg, perfectionist that you are, I thought you might be interested in [this](http://blog.dictionary.com/fewer-vs-less/). I cannot help but grit my teeth when I see "10 items or less" at a supermarket checkout line. – Cary Swoveland Jul 29 '17 at 22:21
2

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.

m. simon borg
  • 2,515
  • 12
  • 20
Dbz
  • 2,721
  • 4
  • 35
  • 53
  • the https://ruby-doc.org/core-2.1.1/Proc.html#method-i-lambda-3F you refer to is great! thanks. The way ruby handles block/proc arguments is kinda messy. – Ze Gao Jul 29 '17 at 12:20
  • @ZeGao Block/proc arguments are not messy, they just follow rules which you're not yet familiar with. Jörg's answer does a better job explaining those rules – m. simon borg Jul 29 '17 at 13:15
  • given `def foo(a, b, *c)`, `foo(1, 1, [1,2,3,4])` and `foo(1, 1, *[1,2,3,4])` are not the same. in the first case `c` is `[[1, 2, 3, 4]]`, in the second it is `[1, 2, 3, 4]`. likewise neither is `foo(1, 2, [3, 4])` the same as `foo(1, 2, *[3, 4])`, the former assigns `c` as `[[3, 4]]`, the latter as `[3, 4]` – m. simon borg Jul 29 '17 at 13:21
  • @m.simonborg you are correct. I fixed that in the answer. – Dbz Jul 29 '17 at 13:51