One of the nicest bits of syntax sugar in Ruby are keywords (**
or double-splat).
Keywords in Method Parameters
Just like the splat operator (*args
) captures any parameters as an Array
of
arguments, the double-splat operator (**attrs
) captures any option parameters
as a Hash
.
The nice thing is that it allows to destructure a Hash
argument into any of
its individual keys, and mark it as required, or make it optional and provide a
default value for it.
def greet(first_name:, last_name: nil, **attrs)
name = [first_name, attrs[:middle_name], last_name].compact.join(' ')
puts "Hi #{ name }!"
end
greet(first_name: 'Gale')
# Hi Gale!
greet(last_name: 'John')
# ArgumentError (missing keyword: first_name)
greet(first_name: 'Bruce', middle_name: 'Wayne', last_name: 'Keaton')
# Hi Bruce Wayne Keaton!
There's one big gotcha which comes up a lot when first learning Ruby:
def greet(**attrs)
puts "Hi #{ attrs[:name] }!"
end
greet
# Hi !
greet('name' => 'Jane')
# ArgumentError (wrong number of arguments (given 1, expected 0))
Many developers have scratched their heads wondering what's going on, until they learn the cause of this unintuitive message.
Only
Symbol
keys are allowed in keyword arguments 😲
Ruby handles keyword arguments differently in its internals, which unfortunately leaks into this error message.
Merging Hashes with the Double-Splat operator
The double-splat operator can also be used to combine hashes. The order matters, when resolving duplicate keys, the rightmost ones will take priority.
jane = { :first_name => "Jane", :last_name => "Doe" }
{ **jane, :last_name => "Johnson" }
# { :first_name => "Jane", :last_name => "Johnson" }
{ :last_name => "Johnson", **jane }
# { :last_name => "Doe, :first_name => "Jane" }
There are a few caveats when combining hashes:
- It does not combine keys of different types, such as
String
andSymbol
. - It can only be used with Hashes where all keys are
Symbol
s.
jane = { :first_name => "Jane", :last_name => "Doe" }
{ **jane, "first_name" => "John" }
# { :first_name => "Jane", :last_name=>"Doe", "first_name"=>"John" }
jane = { "first_name" => "Jane", "last_name" => "Doe" }
doe = { **jane, first_name: "John" }
# TypeError (hash key "first_name" is not a Symbol)
This message used to be very cryptic like the one for keyword arguments, but it has improved a lot since the feature was first added 🎉
A quick note about Rails
When using Rails a developer might think:
I can obtain values from
params
usingSymbol
keys, but I can't pass them as keyword arguments! Thought they were all Symbol keys?
The short answer is no. In Rails controllers, params
is an instance of HashWithIndiferentAccess.
You may index it with Symbol keys but internally it uses String keys.
Fortunately, symbolize_keys
is our friend here, allowing us to transform any
Hash, such as HTTP parameters, to something we can pass as keyword arguments.
Summary
So there you have it, **
is to Hash
what *
is to Array
, and is a very
convenient tool to write shorter, expressive code.
Just watch out for errors when using non-Symbol
keys in keyword arguments,
at least until Ruby improves that error message 😉