Changes to positional and keyword args in Ruby 3.0

Ruby 3.0 introduced a breaking change in how it treats keyword arguments.

There is an excellent blog post on the official Ruby blog going into the details. You should probably read that, but here is a slightly abbreviated version:

What changed

If you call a method that accepts keyword arguments, either explicitly or via **args, you cannot call it with a hash. I.e.

def explicit_kwargs(x:, y:)
  # ...

def splat_kwargs(**kwargs)
  # ...

def no_kwargs(*args)
  # ...

explicit_kwargs(x: 1, y: 2)     # works
explicit_kwargs({x: 1, y: 2})   # raises an ArgumentError
explicit_kwargs(**{x: 1, y: 2}) # works

splat_kwargs(x: 1, y: 2)        # works
splat_kwargs({x: 1, y: 2})      # raises an ArgumentError 
splat_kwargs(**{x: 1, y: 2})    # works

no_kwargs(x: 1, y: 2)           # works, args = [{x: 1, y: 2}]
no_kwargs({x: 1, y: 2})         # also works!

Why it was changed

It was felt that the automatic conversion lead to too many unexpected edge cases and bugs, such as this one.

What will break

The change will be felt most in code that is supposed to work across Ruby 2 and 3, especially when using delegation.

In the olden times, Ruby delegation would be done like this:

def wrapper(*args, &block)
  # do something
  wrapped_method(*args, &block)

In Ruby 3, this will only work if wrapped_method happens to not accept keyword arguments. If it does, this will break. The correct way to do delegation in Ruby 3 is

def wrapper(*args, **kwargs, &block)
  # do something
  wrapped_method(*args, **kwargs, &block)

However, this style will not work correctly in older Rubies. There are differences between Ruby 2.7, and 2.6 and lower, but the effect is that the wrapped_method may end up with an extra or missing empty hash.

To achieve delegation across Ruby versions, if the target method (might) accept keyword args, the recommended way is

ruby2_keywords def wrapper(*args, &block) # You cannot accept **kwargs of any kind here!
  # do something
  wrapped_method(*args, &block)

ruby2_keywords(method_name) was introduced in 2.7. It does some stupidly complex stuff, but basically makes things just work.

In Ruby 2.6 and below you can add the ruby2_keywords-Gem, which will polyfill the method with a noop (since in Ruby 2.6, this style of delegation always works fine anyways).

The "forward everything" syntax

Starting with Ruby 2.7 there is also the following syntax to "forward" everything. I don't consider it very useful, since it is not available on older Rubies, and it prevents you from doing anything with the arguments before forwarding, but it works like this:

def wrapper(...)
Tobias Kraze 4 months ago
This website uses short-lived cookies to improve usability.
Accept or learn more