The splat operator * is commonly used in the argument list of a method so it can accept a variable number of arguments. However this operator can do a lot more. Let’s uncover its secrets.

Splat Operator and Arguments

Here are some examples of how the splat operator can be used in method definitions.

We can capture all arguments:

def foo(*args)
  "args: #{args.inspect}"
end

foo(1, 2, 3, 4) # => "args: [1, 2, 3, 4]"

We can also combine a splatted argument with regular ones:

def foo(first, second, *last)
  "first: #{first.inspect}, second: #{second.inspect}, last: #{last.inspect}"
end

foo(1, 2, 3, 4) # => "first: 1, second: 2, last: [3, 4]"

It’s common to see the splatted argument at the end of argument list, but this is not a requirement:

def foo(*first, last)
  "first: #{first.inspect}, last: #{last.inspect}"
end

foo(1, 2, 3, 4) # => "first: [1, 2, 3], last: 4"

We can even capture middle arguments:

def foo(first, *middle, last)
  "first: #{first.inspect}, middle: #{middle.inspect}, last: #{last.inspect}"
end

foo(1, 2, 3, 4) # => "first: 1, middle: [2, 3], last: 4" foo(1, 2) # => "first: 1, middle: [], last: 2"

Obviously, it is not possible to have multiple splatted arguments:

def foo(*arg1, *arg2); end
# => SyntaxError: (irb):1: syntax error, unexpected *

Splat Operator and yield

We can yield a varying number of argument to the provided block:

def fridge
  food = [:cheddar, :lettuce, :salmon]
  yield *food
end

fridge do |cheese, vegetable, fish|
  "#{cheese}, #{vegetable} and #{fish}"
end
# => "cheddar, lettuce and salmon"

Again, we can use a splat to capture multiple arguments into an array:

fridge do |lunch, *dinner|
  "lunch: #{lunch.inspect}, dinner: #{dinner.inspect}"
end
# => "lunch: :cheddar, dinner: [:lettuce, :salmon]"

We can also group arguments here:

{ a: 'Hello', b: 'World' }.each_with_index do |(key, value), index|
  puts "#{index}: #{key}=>#{value}"
end
# => "0: a=>Hello" # => "1: b=>World"

This is due to the fact that Enumerable#each_with_index yields an array of two elements when we iterate through an hash. The first element is an array containing the current key and value. The second element is the current index.

(key, value), index = [['a', 'Hello'], 0]

key # => "a"
value # => "Hello"
index # => 0

Splat Operator and Parameters

This operator can be used to explode an array in order to pass its values as parameters of a method:

def foo(x, y, z)
  x + y + z
end

a = [1, 2, 3]

# Without splat
foo(a[0], a[1], a[2]) # => 6

# With splat
foo(*[1, 2, 3]) # => 6

More examples:

foo(1, *[2, 3]) # => 6
foo(1, *[2], 3) # => 6
foo(*[1, 2], 3) # => 6

Splat Operator and Arrays

Arrays can be flattened using a splat:

[1, *[*[2, 3], 4]] # => [1, 2, 3, 4]
[1, [[2, 3], 4]].flatten # => [1, 2, 3, 4]

Splat Operator and Assignments

Splat can destructure an array so its values are assigned to multiple variables. If there are more elements in the array than variables, the remaining elements are ignored.

x, y, z = *[1, 2, 3]
x # => 1
y # => 2
z # => 3

x, y, z = *[1, 2, 3, 4]
x # => 1
y # => 2
z # => 3

Same goes for sets:

require 'set'

x, y, z = *Set.new([1, 2, 3])
x # => 1
y # => 2
z # => 3

Putting a splatted argument on the left-hand side will lead to an array splitted into several pieces that will be assigned to separate variables:

head, *tail = [1, 2, 3, 4]
head # => 1
tail # => [2, 3, 4]
head, *body, tail = [1, 2, 3, 4]
head # => 1
body # => [2, 3]
tail # => 4

This might look like using Array#first and Array#last. Well, it depends on the size of the array:

first, *, last = [1, 2, 3]
first # => 1
last # => 3
[1, 2, 3].first # => 1
[1, 2, 3].last # => 3

first, *, last = [1]
first # => 1
last # => nil

[1].first # => 1
[1].last # => 1

As we can see, the name of the splatted argument can be omitted. It can be useful if we want to ignore the corresponding values.

Even better, we can group variables to match the structure of an array:

a, (b, (c, d, e)) = [1, [2, [3, 4, 5]]]
a # => 1
b # => 2
c # => 3
d # => 4
e # => 5

a, (b, (head, *tail)) = [1, [2, [3, 4, 5]]]
a # => 1
b # => 2
head # => 3
tail # => [4, 5]

Implicit Splat

In the following example, the * could be omitted:

x, y, z = *[1, 2, 3]
x # => 1
y # => 2
z # => 3

# same as:
x, y, z = [1, 2, 3]
x # => 1
y # => 2
z # => 3

Why? When the object responds to the to_ary method, an implicit splat occurs. This is the case for Array.

Note that implicit splat can work only if there is one element on the right-hand side of the assignment.

Conversly, we can collapse multiple values into an array:

x = 1, 2, 3 # => [1, 2, 3]

This time, only one element is required on the left-hand side for this trick to work.

Now, let’s implement to_ary.

Without to_ary:

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
end

p = Point.new(12, 68)
x, y = p

x # => #<Point:0x007fbeaa828f78 @x=12, @y=68>
y # => nil

With to_ary:

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  def to_ary
    [@x, @y]
  end
end

p = Point.new(12, 68)
x, y = p

x # => 12
y # => 68

Curiously, *p won’t work in this case:

p = Point.new(12, 68)
x, y = *p
x # => #<Point:0x007fbeab04d410 @x=12, @y=68>
y # => nil

Let’s find out why.

Building Arrays With Splat (Coercion)

Here is a strange feature of the splat operator: coercing objects into arrays.

a = *"abc" # => ["abc"]
a = *123 # => [123]
a = *1..10 # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a = *nil # => []
a = *[1, 2, 3] # => [1, 2, 3]

However, we get an unexpected results with hashes:

a = *{ a: 1, b: 2, c: 3 } # => [[:a, 1], [:b, 2], [:c, 3]]

In our previous example with our custom to_ary, *p didn’t work to destructure our Point object:

p = Point.new(12, 68)
x, y = *p
x # => #<Point:0x007fbeab04d410 @x=12, @y=68>
y # => nil

This is simply because p is coerced into an array whose first and unique value is assigned to x. x, y = *p is equivalent to:

arr = *p # => [#<Point:0x007fbeab04d410 @x=12, @y=68>]
x, y = arr
x # => #<Point:0x007fbeab04d410 @x=12, @y=68>
y # => nil

The Double Splat

The double splat is useful when we want to pass a hash in our argument list.

def foo(**args)
  args
end

foo() # => {}
foo(1, 2, 3) # => ArgumentError: wrong number of arguments (given 3, expected 0)
foo(a: 1, b: 2, c: 3) # => {:a=>1, :b=>2, :c=>3}

We can combine a doubly splatted argument with normal ones.

def foo(first, second, **args)
  "first: #{first}, second: #{second}, args: #{args}"
end

foo(1, 2, a: 3, b: 4, c: 5)
# => "first: 1, second: 2, args: {:a=>3, :b=>4, :c=>5}"

foo(1, 2)
# => "first: 1, second: 2, args: {}"

foo(1, 2, nil)
# => ArgumentError: wrong number of arguments (given 3, expected 2)

foo(1, 2, "not a hash")
# => ArgumentError: wrong number of arguments (given 3, expected 2)

What is the difference between a doubly splatted argument and using a normal variable in argument list ?

def foo(first, second, args = {})
  "first: #{first}, second: #{second}, args: #{args}"
end

foo(1, 2, a: 3, b: 4, c: 5)
# => "first: 1, second: 2, args: {:a=>3, :b=>4, :c=>5}"

foo(1, 2) # => "first: 1, second: 2, args: {}"
foo(1, 2, nil) # => "first: 1, second: 2, args:"
foo(1, 2, "not a hash") # => "first: 1, second: 2, args: not a hash"

As we can see, args can be any type of object, not just a hash. By using a doubly splatted argument we can be certain that args will be a hash. We don’t need to check the type of args inside the method.

What are the differences between a doubly splatted argument and keyword arguments ?

  • a default value can be specified for each keyword argument
  • a keyword argument is mandatory if we did not specified a default value
  • an exception will be thrown if unknown keywords are present when calling the method

Demonstration:

def foo(first: 1, second:)
  "first:#{first}, second:#{second}"
end

foo(second: 2) # => "first:1, second:2"
foo() # => ArgumentError: missing keyword: second
foo(second: 2, third: 3) # => ArgumentError: unknown keyword: third

The double splat can also be used in block parameters.

Antoher quite common use of the doubly splatted operator is when it is combined with a splated argument:

def foo(*bar, **baz)
  "bar: #{bar.inspect}, baz: #{baz.inspect}"
end

foo()
# => "bar: [], baz: {}"

foo(1, 2, 3)
# => "bar: [1, 2, 3], baz: {}"

foo(1, 2, 3, a: 4, b: 5)
# => "bar: [1, 2, 3], baz: {:a=>4, :b=>5}"

foo(1, 2, 3, a: 4, b: 5, 'c' => 6)
# => "bar: [1, 2, 3, {\"c\"=>6}], baz: {:a=>4, :b=>5}"

The last one is quite surprising. In fact, the doubly splatted argument only catch symbol keys.

The double splat has another useful use: merging hashses.

Before:

foo = { third: 3 }
bar = { first: 1, second: 2 }.merge(foo)
# => {:first=>1, :second=>2, :third=>2}

After:

foo = { third: 3 }
bar = { first: 1, second: 2, **foo }
# => {:first=>1, :second=>2, :third=>2}

Conclusion

That’s all for the main uses of the splat operator, which are very broad. The splat operator can be required in some situations or convenient in others. However it can obscure our code in certain cases. As a developer it’s our job to pick the right tool for the right job.