Ruby: Diving Deep with Double-Splat (**)
The double-splat (**) was added to Ruby back when version 2.0 was released, but it seems to have gained very little attention since then. Perhaps it was simply overshadowed by the more-heralded features like lazy enumerators, keyword arguments and Module#prepend, which also arrived in the same version of Ruby. However, the double-splat is a very powerful little feature which can work wonders in tidying up your code.
Simply put, the double-splat is to Hashes what the splat (*) is to Arrays. It picks up trailing
Hash arguments when used in a method signature, and it decomposes a Hash when applied to a method
parameter. Another great feature of the double-splat is that you can use it to “merge” Hash
arguments, eliminating the need to call #merge
explicitly.
Both the splat and double-splat can appear together in the same parameter list. Keyword arguments allow us to specify keys which should not be captured by the double-splat but rather assigned to separate parameters.
Code Without Double-splats
Here is an example of code I recently refactored to take advantage of all the things I have mentioned.
Originally it looked like this:
A few things to note:
- Local variable assignment is needed.
- The name
args
has no meaning. Its a “mixed bag” so-to-speak, whose value is a combination of resources (Array) and options (Hash) which are being passed in. The two must be explicitly separated, in this case by usingextract_options!
(from ActiveSupport). - A call to
merge
adds to the given options.
Improving the Code Using Double-splats
Here is the listing from above, modified to now use the double-splat.
The code is more succinct and, more importantly, clearer than the previous listing. Here are the key improvements:
- The variables have been moved into the method’s argument list. Using both the splat and double-splat args are split automatically into their Array and Hash components. The method arguments’ names now bear meaning and are easier to understand.
- A keyword argument (
parent
) captures a hash key which I want to handle separately from the other options. It is no longer necessary to extract it from the options hash. - When calling
FormPresenter.new
the trailing Hash parameters (the:title
key/value in this case) are merged with double-splattedoptions
, eliminating the need for explicitly calling#merge
.
A Deeper Look
Trailing Hash Arguments (and Magic Merge)
Double-splatted arguments may be mixed and matched with floating hash arguments. All such arguments will be magically merged from left to right. If there are duplicate keys then the last one (right-most) wins out. You can even have multiple double-splatted arguments.
Notably, when a double-splat is applied to an empty Hash
it results in zero arguments. This
is consistent with the behavior of applying a splat to an empty Array
.
Uses Outside of Method Arguments
You can also use double-splats in Hash constructors. It works just like the previous examples.
Block Parameters
Just like with methods, block parameters can use double splats.
Caveats
Double-splat Always Creates New Hash Objects
A double-splatted parameter will always construct a new Hash object as shown here by returning the object id for the options Hash.
Double-splatting an argument will likewise result in the construction of a new Hash.
Only Symbol Keys Are Supported
Trying to double splat a Hash which contains string keys will not work.
Double-splat vs. Optional Parameter
I frequently encounter methods with a optional parameter to capture trailing Hash values (and provide a default value) which looks like this:
In many cases this works great! However, when you add more optional parameters into the signature there is a significant difference between this approach and using a double-splatted parameter. Optional parameters will be satisfied from left to right, which means that if no arguments are given then the trailing Hash values will not be captured by the last parameter.
A double-splat will instead take precedence over optional parameters and capture any trailing Hash arguments.
There may be legitimate use cases for both parameter styles in these examples, but I believe that the double-splat is the best fit in most cases. I was unable to find a real-life example of needing the trailing optional parameter.