Why do Ruby procs/blocks with splat arguments behave differently than methods and lambdas?
Asked Answered
B

2

9

Why do Ruby (2.0) procs/blocks with splat arguments behave differently than methods and lambdas?

def foo (ids, *args)
  p ids
end
foo([1,2,3]) # => [1, 2, 3]

bar = lambda do |ids, *args|
  p ids
end
bar.call([1,2,3]) # => [1, 2, 3]

baz = proc do |ids, *args|
  p ids
end
baz.call([1,2,3]) # => 1

def qux (ids, *args)
  yield ids, *args
end
qux([1,2,3]) { |ids, *args| p ids } # => 1

Here's a confirmation of this behavior, but without explanation: http://makandracards.com/makandra/20641-careful-when-calling-a-ruby-block-with-an-array

Brimstone answered 30/5, 2014 at 1:11 Comment(13)
If you want to improve your question, join... is only making it unnecessarily complicated. It is irrelevant to your question. All you should do is do p ids within each block, and make it clear how it differs.Lukash
Probably has something to do with proc being a standard library method while lambda being a special keyword...Vulpine
Thought you had to new up a Proc?Zootomy
@IdanArye, I added code illustrating that yielding to a block behaves the same. In Ruby 2.0, proc and Proc.new are the same thing: ruby-doc.org/core-2.0/Kernel.html#method-i-procBrimstone
Your added link is a related question, but is not the same. In the issue there, splat is a necessity; it is used to save arity-mismatch . Your question issues more interesting case. The *args makes the arity optional, but the splat is still applied. I suspect it might be a bug.Lukash
ruby-doc.org/core-2.1.1/Proc.html#method-i-lambda-3F (it's called tricks), isn't really an answer to 'why?', but a good explanation.Damascene
This behaviour is so backwards. Lambdas which actually enforce the number of arguments is happy to assign the list to only its first parameter. Proc which doesn't care how many arguments you pass actually spreads the array across its arguments. Intuitively I would expect these behaviours to be reversed.Dioptric
@Lukash it actually does appear to be the same thing, which is the "tricks" referenced in Victor's comment. It occurs when the definition doesn't include the splat too. The issue is that non-"lambda" procs splat their first argument automatically if it's an array. I hacked it in my code by wrapping my first arg array in another array unless lambda? is true, since the outer array will be unwrapped.Brimstone
Meanwhile you can do destructuring in lambdas too: f = ->((x, *xs)) { ... }, so non-lambda block can probably be considered as lambda with implicit parenthesis.Damascene
@IdanArye lambda and proc are both method: [3] pry(main)> method :lambda => #<Method: Object(Kernel)#lambda> I have checked it at Ruby 1.9.3 and 2.0.0.Warily
@DarekNędza OK, this is weird, but in Ruby 2.1.1 lambda is also a keyword. When you use it directly, returning from the lambda's body does not return from the enclosing method, but when you use it via method(:lambda).call it behaves like a regular method-with-block and returning from the block returns from the enclosing method.Vulpine
@IdanArye I cannot check it on the 2.1.* but at 2.0 & 1.9.3 lambda/-> returns from block BUT proc/Proc.new raises LocalJumpError: unexpected return. It is indeed weird, because in the mentioned versions lambda works as I described. It might be 2.1.* bug.Warily
@VictorMoroz Please feel free to add an answer summarizing your knowledge and I'll accept it. You definitely identified the behavior, even if we don't know why anyone would want to always splat a single array passed to a block...Brimstone
D
3

There are two types of Proc objects: lambda which handles argument list in the same way as a normal method, and proc which use "tricks" (Proc#lambda?). proc will splat an array if it's the only argument, ignore extra arguments, assign nil to missing ones. You can partially mimic proc behavior with lambda using destructuring:

->((x, y)) { [x, y] }[1]         #=> [1, nil]
->((x, y)) { [x, y] }[[1, 2]]    #=> [1, 2]
->((x, y)) { [x, y] }[[1, 2, 3]] #=> [1, 2]
->((x, y)) { [x, y] }[1, 2]      #=> ArgumentError
Damascene answered 31/5, 2014 at 13:8 Comment(0)
S
1

Just encountered a similar issue!

Anyways, my main takeaways:

  1. The splat operator works for array assignment in a predictable manner

  2. Procs effectively assign arguments to input (see disclaimer below)

This leads to strange behavior, i.e. the example above:

baz = proc do |ids, *args|
  p ids
end
baz.call([1,2,3]) # => 1

So what's happening? [1,2,3] gets passed to baz, which then assigns the array to its arguments

ids, *args = [1,2,3]
ids = 1
args = [2,3]

When run, the block only inspects ids, which is 1. In fact, if you insert p args into the block, you will find that it is indeed [2,3]. Certainly not the result one would expect from a method (or lambda).

Disclaimer: I can't say for sure if Procs simply assign their arguments to input under the hood. But it does seem to match their behavior of not enforcing the correct number of arguments. In fact, if you give a Proc too many arguments, it ignores the extras. Too few, and it passes in nils. Exactly like variable assignment.

Subminiaturize answered 12/6, 2014 at 22:33 Comment(1)
Haha this is exactly what I was doing that triggered this issue. See here and here for the workaround I used, which was basically to wrap ids in an array unless lambda?. In application code, this hack probably wouldn't be necessary, but it's used in the library code here because I wanted to allow a lambda, a wrapped proc, or an unwrapped block as a method argument.Brimstone

© 2022 - 2024 — McMap. All rights reserved.