WTF Ruby Keyword Args

A couple weeks back I encountered an edge-case with Ruby keyword arguments that left me scratching my head. It took some time on Google and IRC before I understood what was going on. I decided to document this to hopefully save someone time / frustration in the future.

Take this straight-forward method definition:

def kw(foo: 'one')
  puts foo

You can call it in a couple of ways:

# => "one"

kw(foo: 'two')
# => "two"

So far, so good. Now, what happens when a hash is passed as the only argument?

kw({ foo: 'three' })
# => "three"

This threw me for a loop. At the time of writing, this behavior was undocumented (I am working to fix that). It's hard to put into words, but something about this feels off; not wrong, just hard to rationalize.

It was probably made worse by how I encountered this particular issue. I had an object that acted as a delegator for another object. The delegator was to inject some default value for specific keyword arguments; all without the calling code or the object being delegated to knowing.

Here's a simplified version of the problem that I documented in my WTF Keyword Args gist:

class MM
  def method_missing(method, *args, **kargs)

def foo(data, one: 1)
end{ foo: 'bar' }, one: 1)
# works fine'bar')
# also works fine{ foo: 'bar' })
# valid signature, but raises ArgumentError
# this is due to an edge-case with keyword args where if the last value 
# in a method call is a hash the value is passed as keyword args

Well, that's annoying. And I don't know of a workaround.

In all of my reading on keyword args in Ruby (which admittedly isn't that much), I've never seen this edge-case mentioned. I imagine this behavior was added for backwards compatability? Maybe there's more to it; either way I'd like to better understand the decision here.