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

is incorrect on Ruby 3.0.

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.