Correct Delegation with Ruby 2.6, 2.7 and 3.0
While looking at how a few gems handle delegation on Ruby 2.7, I noticed that many of them are unfortunately incorrect. The official blog post about keyword arguments changes in Ruby 2.7 and Ruby 3.0 is rather long and might be unclear. So I will keep this one really short and to the point.
I am a CRuby committer which discussed and designed the keyword arguments changes along with Jeremy Evans and Yusuke Endoh.
The Only Correct Way
There is actually just a single way to do delegation that works in Ruby <= 2.6, in Ruby 2.7 and in Ruby >= 3:
def foo(*args, &block)
target(*args, &block)
end
ruby2_keywords :foo if respond_to?(:ruby2_keywords, true)
It’s simple: ruby2_keywords
is the only way to achieve correct delegation on Ruby 2.7.
The approach above is used in Rails, etc.
You can also add the ruby2_keywords
gem to remove the respond_to?
check and be able to use:
ruby2_keywords def foo(*args, &block)
target(*args, &block)
end
Some Frequent Incorrect Ways
# Broken on 2.6 and 2.7
def foo(*args, **kwargs, &block)
target(*args, **kwargs, &block)
end
is incorrect on Ruby 2.6 and on Ruby 2.7.
# Broken on Ruby 3+, warns in Ruby 2.7
def foo(*args, &block)
target(*args, &block)
end
Some try a combination of the above:
# Broken on 2.7
if RUBY_VERSION < "2.7"
def foo(*args, &block)
target(*args, &block)
end
else
def foo(*args, **kwargs, &block)
target(*args, **kwargs, &block)
end
end
But that’s still incorrect on Ruby 2.7
(like the first example, foo({})
passes no arguments at all).
Conclusion
Use ruby2_keywords
for delegation, that’s the only correct way, as shown above.
If you maintain some Ruby code or a gem, search for methods delegating (= receiving + passing) keyword arguments with **
,
like the first and third incorrect patterns.
Delegation with **
does not work in Ruby 2.6 and 2.7, use ruby2_keywords
+ *args
instead.
All methods delegating with *args
should use ruby2_keywords
for Ruby >= 2.7, as long as the target method might have keyword arguments.
Post Scriptum
Needing to use ruby2_keywords
explicitly for delegation is unfortunate, I wish there would be a more natural way to express delegation in Ruby 2.7.
Unfortunately there is not.
I once proposed to enable ruby2_keywords
by default to preserve compatibility, but this was rejected.
It would also have provided better warnings for delegation.
We could have postponed the delegation-related changes in order to migrate delegation code once (*args
-> *args, **kwargs
) and not twice (*args
-> ruby2_keywords
-> *args, **kwargs
/ (...)
),
but matz wanted to make all keyword arguments deprecations at once.
At least now we have Ruby 3.0, so one can more easily find problematic delegation code as it just breaks on Ruby 3.0 if not done correctly.
Note that keyword argument warnings are disabled by default in Ruby 2.7.2.
Use RUBYOPT=-W:deprecated command
or Warning[:deprecated] = true
in Ruby code to see them.
There is the def foo(...); target(...); end
syntax but it fails to parse on Ruby <= 2.6,
and only works with no other arguments at all in Ruby 2.7.0 - 2.7.2,
therefore it has extremely limited applicability these days and I just ignore it.
Note that (arg, ...)
might finally get backported to 2.7.3.
It might make sense to consider ...
when dropping Ruby 2.6 support.
When dropping Ruby 2 support entirely, one can switch to (*args, **kwargs)
-style delegation, as that works correctly on Ruby >= 3.
Another way would be to explicitly not support Ruby 2.7, then the last example with the version check would work. It is probably impractical for most gems to do that though.