Ruby 2.7+ deprecates the ability to automatically translate between a hash and keyword arguments for the last argument in a method invocation. This will be removed in Ruby 3.0 due to some ambiguity in various cases. For a more in-depth rundown, you can check the writeup on ruby-lang.org. This post is more about a simple way to solve deprecation warnings of existing code if you are upgrading to Ruby 2.7.
The Deprecation
It’s fairly common for codebases with so-called “service objects” to include a class method to make invocations look a bit cleaner, for example:
class DoTheThing
def self.call(*args)
new.call(*args)
end
def call(a:, b:)
a + b
end
end
This lets us call DoTheThing.call(<args>)
rather than the slightly more verbose DoTheThing.new.call(<args>)
. This works fine on older versions of Ruby, but run on 2.7+ and you’ll get a deprecation warning (I’m running 2.7.1 in the examples):
DoTheThing.call(a: 1, b: 2)
warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
=> 3
Option A - The Double Splat
You can, of course, just wrap the arguments in a hash and double-splat it when passing (as the deprecation message suggests), like so:
DoTheThing.call(**{a: 1, b: 2})
=> 3
This works and removes the warning. Unfortunately, it’s not particularly nice to look at (I’d rather just go with .new.call(a: 1, b: 2)
if this was the alternative). Not only that, but you also have to update every single place that is calling one of these methods.
Option B - The ‘Forward All Arguments’ Operator
There is another option: the ‘Forward All Arguments’ operator: ...
, which strangely I don’t see mentioned in many places that talk about Ruby’s keyword argument change. Our example class above can then be reworked to use it like so:
class DoTheThing
def self.call(...)
new.call(...)
end
def call(a:, b:)
a + b
end
end
The original invocation now works like it did on Ruby 2.6 and produces no warning:
DoTheThing.call(a: 1, b: 2)
=> 3
Caveats
Probably the main catch with this operator is that it only works if you are directly passing through all arguments without any need to inspect or alter them in any way. The parenthesis around ...
are also required, I assume so the Ruby interpreter can tell you’re not trying to access a method via the first .
.