Ruby 3.0 introduced a breaking change in how it treats keyword arguments.
There is an excellent blog post Show archive.org snapshot on the official Ruby blog going into the details. You should probably read that, but here is a slightly abbreviated version:
What changed
When the last argument of a method call is a Hash, Ruby < 3 automatically converted it to to Keyword Arguments. If you call a method in Ruby >= 3 that accepts keyword arguments, either explicitly or via **args
, you cannot call it with a hash. I.e.
def explicit_kwargs(x:, y:)
# ...
end
def splat_kwargs(**kwargs)
# ...
end
def no_kwargs(*args)
# ...
end
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)
end
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)
end
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)
end
ruby2_keywords(method_name)
was introduced in 2.7. It does some
stupidly complex stuff
Show archive.org snapshot
, 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(...)
wrapped_method(...)
end