Exploring Ruby: Splat, splat and double splat!

dabrorius

Filip Defar

Posted on May 6, 2019

Exploring Ruby: Splat, splat and double splat!

Ruby has a quite flexible way of defining methods that accept arbitrary number of arguments.
We run into splat operators often, so I wanted to take some time and explore them in a bit more depth.

Let's start with a question that might intrigue you into exploring this topic.
If we have the following method call:

my_method('Go', 'Now', name: 'John Goblikon')
Enter fullscreen mode Exit fullscreen mode

What will be assigned as arguments when the method is defined in following ways?

def my_method(*args)
Enter fullscreen mode Exit fullscreen mode
def my_method(**args)
Enter fullscreen mode Exit fullscreen mode
def my_method(*args1, **args2)
Enter fullscreen mode Exit fullscreen mode

Let's find out!

Splat operator (*)

Splat operator allows us to convert an Array to a list of arguments and vica versa.

Let's look at an example function that takes arbitrary number of arguments.

def things_i_like(*things)
  things.each { |thing| puts "I like #{thing}" }
end
Enter fullscreen mode Exit fullscreen mode

We can call this method like this:

things_i_like('pie', 'doggos', 'grog')
# => I like pie
# => I like doggos
# => I like grog
Enter fullscreen mode Exit fullscreen mode

All passed arguments will be collected into an Array and passed as a single argument named things.

things # => ["pie", "doggos", "grog"]
things.class # => Array
Enter fullscreen mode Exit fullscreen mode

Mixing it up

We can also define regular positional parameters in combination with a splat parameter.

def likes(main_interest, *others)
  puts "I like #{main_interest} the most!"
  others.each do |other|
    puts "I also like #{other}"
  end
end
Enter fullscreen mode Exit fullscreen mode
likes('pokemon', 'cats', 'chairs')
# => I like pokemon the most!
# => I also like cats
# => I also like chairs
Enter fullscreen mode Exit fullscreen mode

This type of parameters is called "positional parameters" for a reason - the order of arguments decides how they will be assigned.

In this case, the first argument is separated from the rest. However, the splat argument does not have to be the last argument. Doing something like this is completely acceptable:

def likes(main_interest, *others, last_one)
Enter fullscreen mode Exit fullscreen mode

However, there is a limit to how crazy we can get. It is not allowed to have more than one splat parameter in the same method.

def likes(main_interest, *others, last_one, *and_some_more)
# => syntax error, unexpected *
Enter fullscreen mode Exit fullscreen mode

Putting it in reverse

The splat operator also works the other way around.
It can convert an Array into a list of arguments!

def three_things_i_like(a, b, c)
  puts "I like #{a}, #{b} and #{c}."
end
Enter fullscreen mode Exit fullscreen mode

We can now call this method with three arguments as usual.

three_things_i_like('pie', 'doggos', 'grog')
Enter fullscreen mode Exit fullscreen mode

However, we can use the splat operator to pass in an Array instead.

array_of_likes = %w(pie doggos grog)
three_things_i_like(*array_of_likes)
Enter fullscreen mode Exit fullscreen mode

This can be quite useful. but we should be cautious! If the array does not have exactly three elements we will get an ArgumentError.

array_of_likes = %w(pie doggos)
three_things_i_like(*array_of_likes)
# =>  wrong number of arguments (2 for 3) (ArgumentError)
Enter fullscreen mode Exit fullscreen mode

Named parameters

Ruby 2.0 introduced keyword arguments, and splat argument works with them. Splat argument always creates an Array, and all unmatched keyword arguments will be collected in a Hash that will be the last element of that array.

def splat_test(*args)
  args
end
Enter fullscreen mode Exit fullscreen mode
splat_test('positional')
# => ['positional']
Enter fullscreen mode Exit fullscreen mode
splat_test('positional', foo: 'bar')
# => ['positional', {:foo=>"bar"}]
Enter fullscreen mode Exit fullscreen mode
splat_test(foo: 'bar')
# => [{:foo=>"bar"}]
Enter fullscreen mode Exit fullscreen mode

The single splat operator works in reverse even for named parameters. We just need to add a Hash as the last element of the array.

def introduction(title, name:, surname:)
  puts "Hello #{title} #{name} #{surname}"
end
Enter fullscreen mode Exit fullscreen mode
args = ['Mr.', { name: 'John', surname: 'Goblikon' }]
introduction(*args)
Enter fullscreen mode Exit fullscreen mode

Double splat operator (**)

Double splat operator works similar to the splat operator, but it only collects keyword arguments. For this reason, it always generates a Hash, not an Array.

def do_something(**options)
  options.each { |k, v| puts "Options #{k}: #{v}" }
end
Enter fullscreen mode Exit fullscreen mode
do_something(color: 'green', weight: 'bold')
# => Options color: green
# => Options weight: bold
Enter fullscreen mode Exit fullscreen mode
things # => {:color=>"green", :weight=>"bold"}
things.class # => Hash
Enter fullscreen mode Exit fullscreen mode

Double splat operator will collect only named arguments that were not matched to regular parameters. This behavior is similar to the single splat operator, but instead of relying on the position of arguments it relies on their names.

def do_something(action:, **options)
  puts "Action: #{action}"
  options.each { |k, v| puts "Options #{k}: #{v}" }
end
Enter fullscreen mode Exit fullscreen mode
do_something(action: 'print', color: 'green', weight: 'bold')
# => Action: print
# => Options color: green
# => Options weight: bold
Enter fullscreen mode Exit fullscreen mode

Double splat operator also works in reverse. You can convert a Hash into named parameters.

def print_multiple(value:, count:)
  count.times { puts value }
end
Enter fullscreen mode Exit fullscreen mode
hash_params = { value: 'Hello', count: 3 }
print_multiple(**hash_params)
# => Hello
# => Hello
# => Hello
Enter fullscreen mode Exit fullscreen mode

It's important to notice that keys of that hash must be symbols. They can't even be strings.

hash_params = { 'value' => 'Hello', 'count' => 3 }
print_multiple(**hash_params)
# => wrong argument type String (expected Symbol) (TypeError)
Enter fullscreen mode Exit fullscreen mode

Using both splat operators

You can mix both splat and double splat parameters within the same method.

If both are present at the same time, the single splat operator will collect only positional arguments while the double splat operatr will behave as usual. It will collect all unmatched named arguments.

def can_you_do_this?(*positional, **named)
  puts positional
  puts named
end
Enter fullscreen mode Exit fullscreen mode
can_you_do_this?('first', 'second', name: 'john', surname: 'doe')
# => first
# => second
# => {:name=>"john", :surname=>"doe"}
Enter fullscreen mode Exit fullscreen mode

You can define positional, splat, keyword and double splat parameters inside the same method. However, the order is important here. Keyword arguments need to go after positional arguments, and the double splat operator must be at the very end.

def can_you_do_this?(first, *positional, second, name:, **named)
# This is perfectly ok!
Enter fullscreen mode Exit fullscreen mode

Conclusion

Even though it might look a bit intimidating at first, the splat operator logic is actually quite straightforward.

Just keep in mind that:

  • Regular arguments have precedence and will be assigned first.
  • Double splat always works only on named arguments.
  • Single splat collects both positional and named arguments unless there's also a double splat parameter defined in the same method.

Solution

So to answer the question from the beginning. What will happen with this method call?

my_method('Go', 'Now', name: 'John Goblikon')
Enter fullscreen mode Exit fullscreen mode

Example 1

The first method is ok and it will collect all arguments into an Array, with named arguments stored as a Hash.

def my_method(*args)
# args => ['Go', 'Now', {name: 'John Goblikon'}]
Enter fullscreen mode Exit fullscreen mode

Example 2

The second example is invalid, we are only collecting named arguments while two positional arguments are passed to the method. We will get an ArgumentError.

def my_method(**args)
# wrong number of arguments (2 for 0) (ArgumentError)
Enter fullscreen mode Exit fullscreen mode

Example 3

The third example is again valid. This time the single splat argument will collect only positional arguments, while the double splat will collect the named arguments.

def my_method(*args1, **args2)
# args1 => ['Go', 'Now']
# args2 => {name: 'John Goblikon'}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
dabrorius
Filip Defar

Posted on May 6, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related