How does Ruby unpack arguments in Proc?

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

      

The 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 syntactic sugar for a_proc.call (2,2 , 4.3) to hide the "challenge"

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 looks like it differs from a regular function call because it doesn't check the number arguments passed in.

However, as expected, the method that invokes the semantics throws an error when using parameters similarly

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 don't think there is such an inconsistency as a design glitch, so any ideas on this would be appreciated.

+3


source to share


2 answers


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

Block semantics are more like assignment semantics than method semantics in this regard. In fact, in older versions Ruby locks literally used parameter binding assignment, 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

      

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

  • If a block has multiple parameters, but only receives one argument, a message will be sent to the argument to_ary

    (if it is not already Array

    ) and the parameters will be bound to the elementsArray

  • If the block receives more arguments than it has parameters, it ignores the additional arguments
  • If the block receives fewer arguments than it has parameters, the additional parameters are bound to nil

Note: # 1 is what makes it Hash#each

work so nicely, otherwise you always have to deconstruct the array it goes into a block.

In short, block parameters are bound in the same way as with multiple assignments. You can imagine assignment without setters, indices, globals, instance variables and class variables, only local variables, and this is largely related to binding parameters to blocks: copy and paste the parameter list from the block, copy and paste the argument list from yield

, put the sign =

between you and get this idea.

Now you are not actually talking about the block, but you are talking about Proc

. To do this, you need to know something important: there are two types of Proc

s that are unfortunately implemented using the same class. (IMO, they had to be two different classes.) One kind is called a lambda and the other is usually called a proc (confusingly, since both are Proc

s).

Procs behave like blocks, both when binding parameters and passing arguments (i.e. the assignment semantics described above), and also when it comes to behavior return

(it is returned from the nearest lexically enclosing method).

Lambdas behave like methods, both in the case of parameter binding, and when passing arguments (i.e. strict argument validation), and also when it comes to behavior return

(it is returned from the lambda itself).

Simple mnemonic: "block" and "proc" rhyme, "method" and "lambda" are Greek.




A small note to your question:

a_proc [2,2,4,3] is just syntactic sugar for a_proc.call (2,2,4,3) to hide the "call"

This is not syntactic sugar. Rather, it Proc

simply defines a method []

to behave identically . call

What is syntactic sugar:

a_proc.(2, 2, 4, 3)

      

Every occurrence

foo.(bar, baz)

      

interpreted as

foo.call(bar, baz)

      

+4


source


I believe I might confuse you some of the Procs properties. If they are given one array argument, they will automatically split it. Also, ruby ​​blocks generally have some interesting ways to handle block arguments. The behavior you expect is what you get with the 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 statement, and then move on 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

      

Thus, the error makes sense in your argument: def foo()

takes at least two arguments a

, b

and many of them *c

. *

splat operator . It will turn the array into separate arguments, or otherwise here a variable number of arguments into an array. So when you speak foo([1,2,3,4])

, you give foo

one argument a

, and that [1,2,3,4]

. You don't install b

or *c

. Which will work foo(1, 1, 1, 2, 3, 4])

, for example, because you install a

, b

and c

. It will be the same: foo(1, 1, *[1,2,3,4])

.

Now foo(*[1, 2, 3, 4])

works as expected because the splat ( *

) operator turns this into foo(1, 2, 3, 4)

or equivalentfoo(1, 2, *[3, 4])



So now that we have the splat statement, consider the following code (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 a single argument is given for / procs blocks array

, it will automatically be splat

that one. 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 get, as output 1:1

, 2:2

and 3:3

. Since each element is passed as an argument to the block, it sees that it is an array and displays it, assigning as many block data as possible to the elements. Instead of just putting this array in a

, for example a = [1, 1]; b = nil

, you get a = 1; b = 1

. It does the same with proc.

a_proc.call([1, 2, 3, 4])

turns into Proc.new { |1, 2, [3, 4]| c.map { |i| i * b }}

and displays [6, 8]

. It splits up its arguments automatically.

+2


source







All Articles