Search code examples
rubymethod-invocation

Why is ** optional when "splatting" keyword arguments?


Given this method definition:

def foo(a = nil, b: nil)
  p a: a, b: b
end

When I invoke the method with a single hash argument, the hash is always implicitly converted to keyword arguments, regardless of **:

hash = {b: 1}
foo(hash)     #=> {:a=>nil, :b=>1}
foo(**hash)   #=> {:a=>nil, :b=>1}

I can pass another (empty) hash as a workaround:

foo(hash, {}) #=> {:a=>{:b=>1}, :b=>nil}

But, this looks pretty cumbersome and awkward.

I would have expected Ruby to handle this more like arrays are handled, i.e.:

foo(hash)     #=> {:a=>{:b=>1}, :b=>nil}
foo(**hash)   #=> {:a=>nil, :b=>1}

And using literals:

foo({b: 1})   #=> {:a=>{:b=>1}, :b=>nil}
foo(b: 1)     #=> {:a=>nil, :b=>1}
foo(**{b: 1}) #=> {:a=>nil, :b=>1}

The current implementation looks like a flaw and the way I was expecting it to work seems obvious.

Is this an overlooked edge case? I don't think so. There's probably a good reason that it wasn't implemented this way.

Can someone enlighten me, please?


Solution

    1. As for the lack of ** part:

      My guess is that, to make method invocation simple, Ruby always once interprets the key: value form without the braces as a hash with omitted braces, whether it is actually going to be interpreted as such hash or as keyword arguments.

      Then, in order to interpret that as keyword arguments, ** is implicitly applied to it.

      Therefore, if you had passed an explicit hash, it will not make difference to the process above, and there is room for it to be interpreted either as an actual hash or as keyword arguments.

      What happens when you do pass ** explicitly like:

      method(**{key: value})
      

      is that the hash is decomposed:

      method(key: value)
      

      then is interpreted as a hash with omitted braces:

      method({key: value})
      

      then is interpreted either as a hash or as a keyword argument.

    2. As for keyword arguments having priority over other arguments, see this post on Ruby core: https://bugs.ruby-lang.org/issues/11967.