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

Jordan
  • 391
  • 4
  • 12
  • 1
    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. – sawa May 30 '14 at 01:35
  • Probably has something to do with `proc` being a standard library method while `lambda` being a special keyword... – Idan Arye May 30 '14 at 01:40
  • Thought you had to new up a Proc? – Tony Hopkinson May 30 '14 at 01:46
  • @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: http://www.ruby-doc.org/core-2.0/Kernel.html#method-i-proc – Jordan May 30 '14 at 01:47
  • 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. – sawa May 30 '14 at 02:16
  • 3
    http://www.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. – Victor Moroz May 30 '14 at 02:18
  • 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. – Martin Konecny May 30 '14 at 02:42
  • @sawa 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. – Jordan May 30 '14 at 04:27
  • Meanwhile you can do destructuring in lambdas too: `f = ->((x, *xs)) { ... }`, so non-lambda block can probably be considered as lambda with implicit parenthesis. – Victor Moroz May 30 '14 at 13:54
  • @IdanArye `lambda` and `proc` are both method: `[3] pry(main)> method :lambda => #` I have checked it at Ruby 1.9.3 and 2.0.0. – Darek Nędza May 30 '14 at 13:57
  • @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. – Idan Arye May 30 '14 at 14:24
  • @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. – Darek Nędza May 30 '14 at 17:29
  • @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... – Jordan May 30 '14 at 20:50

2 Answers2

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
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
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.

phil-ociraptor
  • 1,041
  • 8
  • 5
  • Haha this is _exactly_ what I was doing that triggered this issue. See [here](https://github.com/jordansexton/active_relation/blob/master/lib/active_relation/include.rb#L26) and [here](https://github.com/jordansexton/active_relation/blob/master/lib/active_relation/associations.rb#L218) 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. – Jordan Jun 13 '14 at 18:26