Read more

Heads up: Ruby implicitly converts a hash to keyword arguments

Dominik Schöler
September 30, 2015Software engineer at makandra GmbH

When a method has keyword arguments, Ruby offers implicit conversion Show archive.org snapshot of a Hash argument into keyword arguments. This conversion is performed by calling to_hash on the last argument to that method Show archive.org snapshot , before assigning optional arguments. If to_hash returns an instance of Hash, the hash is taken as keyword arguments to that method.

Issue

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

If you have ...

  • an object that defines to_hash (may well be a simple Hash instance) and
  • pass it to a method with optional arguments and keyword arguments

... then it is not set as the first optional argument. Instead, Ruby calls to_hash on the object and tries to match the result to keyword arguments. If the hash contains unsupported keys (which it most likely will), Ruby will raise ArgumentError. Bah. (If to_hash does not return a Hash, assignment will work as expected – but this should actually never happen.)

I consider this overeager of Ruby. I will call to_hash myself, or simply use the **double splat operator, if I need to.

Update: This is not a bug, it's a misunderstood feature of Ruby. There are pairs of conversion methods like to_h and to_hash, of which the former is for explicit conversion and the latter for implicit conversion. See this card for details.

Example

def method(arg = 'arg', kw_arg: 'kw_arg')
  [arg, kw_arg]
end

# As expected:
method() # => ['arg', 'kw_arg']
method(kw_arg: 'your keyword') # => ['arg', 'your keyword']

# Intended as nicety: implicit hash conversion
method({kw_arg: 'hash kw_arg'}) # => ['arg', 'hash kw_arg']

# But has bad side effects:
o = String.new('example object')
def o.to_hash # Now o responds to #to_hash
  { kw_arg: 'o.to_hash' }
end

method(o)
# => ['arg', 'o.to_hash']
# Ruby thinks that o is a Hash and converts it to keyword arguments -.-

method(o, o)
# => ['example object', 'o.to_hash']
# Same here, but since only the *last* argument is converted,
# the first is properly assigned to the first optional argument

Workaround

Just don't mix optional and keyword arguments.

Update: Actually, do not define to_hash when you need it for explicit conversion to a Hash. Define to_h instead.

Posted by Dominik Schöler to makandra dev (2015-09-30 11:20)