Keyword arguments unpacking (splat) in Ruby

2020-02-12 02:07发布

问题:

What is happening below seems a little strange to me.

def f(a, b)
  puts "#{a} :: #{b}"
end

f(*[1, 2], **{}) # prints "1 :: 2"

hash = {}
f(*[1, 2], **hash)
ArgumentError: wrong number of arguments (3 for 2)

f(*[1, 2], **Hash.new)
ArgumentError: wrong number of arguments (3 for 2)

Is this a compiler optimization feature?

回答1:

That is a Ruby's bug that has been reported several times (for example here by me) but has not been fixed.

I guess that since the keyword argument feature has been introduced, the double splat syntax has become murky, and that is the indirect cause of this bug. I heard that Matz is considering of introducing a new syntax in some future version of Ruby to distinguish hashes and keyword arguments.



回答2:

[Edit: I saw @sawa's answer after completing mine. I was right: it's a bug!]

That different results are obtained when a literal empty hash is double-splatted and an empty hash that is the value of a variable is double-splatted, seems to me to be prima facia evidence that it's due to a bug in Ruby. To understand why the bug may exist, consider first the reason for passing a double-splatted hash to a method.

Suppose we define a method with some keyword arguments:

def my_method(x, a: 'cat', b: 'dog')
  [x, a, b]
end

my_method(1)
  #=> [1, "cat", "dog"] 

The defaults apply for the two keyword arguments. Now try:

my_method(1, a: 2)
  #=> [1, 2, "dog"]

Now lets use a double-splatted hash.

h = { a: 2, b: 3 }

my_method(1, **h)
 #=> [1, 2, 3] 

This works the same with required keyword arguments (Ruby 2.1+).

def my_method(x, a:, b:)
  [x, a, b]
end

my_method(1, **h)
  #=> [1, 2, 3]

However, to use a double-splatted hash as an argument, the hash cannot contain keys that are not listed as arguments in the method definition.

def my_method(x, a:)
  [x, a]
end

h = { a: 2, b: 3 }

my_method(1, **h)
  #=> ArgumentError: unknown keyword: b

The question therefore arises: can a double-splatted empty hash be passed as an argument, considering that all the hash's keys (none) are included as arguments in the method definition (which case it would have no effect)? Let's try it.

def my_method(x)
  [x]
end

my_method(1, **{})
  #=> [1]

Yes!

h = {}
my_method(1, **h)
  #=> ArgumentError: wrong number of arguments (given 2, expected 1)

No!

That makes no sense. So assuming this is a bug, how might it have arisen? I suspect it may have to do with an optimization of Ruby, as suggested by the OP. It the empty hash is a literal, one could deal with it earlier in Ruby's code than if it were the value of variable. I'm guessing that whoever wrote the earlier code answered "yes" to the question I posed above, and whoever wrote the latter code answered "no", or failed to consider the case of an empty hash at that point.

If this bug theory is not shot down, the OP or someone else should report it.



回答3:

I have a feeling you're tripping over a peculiarity of the splat operator in relation to that empty hash structure. It seems that splatting an empty inline hash causes it to disappear, but anything else gets expanded as an argument of some sort.

This may in fact be a bug in Ruby, though it's such a quirky edge case I'm not really surprised.

Your f function does not accept keyword arguments of any sort, so if a sufficiently vigorous attempt is made to supply them it'll fail out. The last two examples seem to be trying to force in an empty hash as a literal argument.