Keyword arguments unpacking (splat) in Ruby

2020-02-12 02:21发布

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?

3条回答
Ridiculous、
2楼-- · 2020-02-12 02:27

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.

查看更多
可以哭但决不认输i
3楼-- · 2020-02-12 02:33

[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.

查看更多
我欲成王,谁敢阻挡
4楼-- · 2020-02-12 02:38

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.

查看更多
登录 后发表回答