4

According to this post, it is possible to have an optional key word argument after a splat argument. This works if the splat argument is introducing an array of arrays, but not when it is an array of hashes

For example if the method being called is defined as

def call(*scores, alpha: nil)
  puts scores
end

then this works

scores = [[1,2],[3,4]]
call(*scores)

but this does not

scores = [ {a: 1}, {b: 3}]
call(*scores)

giving the following (with ruby 2.4.4)

ArgumentError: unknown keyword: b

but this works

scores = [ {a: 1}, {b: 3}]
call(*scores, alpha: nil)

What is going wrong here?

Obromios
  • 15,408
  • 15
  • 72
  • 127
  • Interesting article. Are you saying that splats work in a different way when the sub elements are hashes, and so that is why it is not working? If so, do you want to write up an answer that I can accept. – Obromios Mar 14 '19 at 00:30
  • I updated my answer which may also help you understand this better. – lacostenycoder Mar 14 '19 at 01:34

2 Answers2

3

The splat operator splits the array into arguments.

However if you wrap it in an array it works again but now it's an array inside an array and still treated as a single argument passed to your method.

call([*scores]) #no error

But also to illustrate why you got the error look what happens here:

def call(*scores, alpha: nil)
  puts scores.inspect
end

call(*scores[0]) #=> #[[:a, 1]]

UPDATED: Thanks to @Stefan, the reason for the error is actually that your method accepts keyword arguments, which is apparently a known bug. See Keyword arguments unpacking (splat) in Ruby

The reason your last example works is that by passing a 2nd argument to your method, the splat handles the first argument as an array instead of trying to split it up into 2 arguments.

Fore more see Ruby, Source Code of Splat?

Also see https://www.rubyguides.com/2018/07/ruby-operators/#Ruby_Splat_Operator

lacostenycoder
  • 10,623
  • 4
  • 31
  • 48
  • _"it has unintended side-effects when you call it on an array of hashes"_ – an array of hashes is still an array. Furthermore, if `scores` is an array, `call([*scores])` is equivalent to `call(scores)` so you'll end up wrapping your array in an extra array within the method, i.e. `[[{a: 1}, {b: 3}]]` – Stefan Mar 14 '19 at 09:03
  • 1
    _"Your hash gets converted to an array"_ – only if you splat a hash. But in the OP's example, `scores` is an array. `*scores` does **not** convert the hashes within `scores` to arrays. The error is caused by passing multiple hashes to a method that also accepts keyword arguments. – Stefan Mar 14 '19 at 12:49
2

* converts the array elements to an argument list, so:

call(*[{a: 1}, {b: 3}])

is equivalent to:

call({a: 1}, {b: 3})

Ruby also converts hashes to keyword arguments implicitly (without **), so the above is equivalent to:

call({a: 1}, b: 3)

Therefore, {a: 1} is treated as a positional argument, and b: 3 (or {b: 3}) as a keyword argument. And because call doesn't take a keyword argument named b you get ArgumentError: unknown keyword: b.

To avoid this, you can pass an extra empty hash as the last argument:

call({a:1}, {b:2}, {})

or:

call(*[{a:1}, {b:2}], {})

or

scores = [{a:1}, {b:2}]
call(*scores, {})

There's a feature request to add "real" keyword arguments in Ruby 3.


IMO, it would be more correct to use call(*scores, **{}) to indicate "no keyword arguments", but due to a bug this currently doesn't work. You could however use call(*scores, **Hash.new)

Stefan
  • 109,145
  • 14
  • 143
  • 218