Ruby Keywords and the Double Splat Operator

AuthorMáximo Mussini
·3 min read

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 and Symbol.
  • It can only be used with Hashes where all keys are Symbols.
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 using Symbol 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 😉