This blog is a part of my "rework" series where I rewrite a blog post that I wrote while I was an apprentice software developer. I have lots more knowledge and want to revisit these old posts, correct where I'm wrong, and
expand where I now have more in-depth experience. If you'd like to, you can read my initial post about Ruby's splat operator.
What is the splat operator?
Programmers call !
bangs, .
dots, =>
hash rockets (this one may be Ruby specific), and *
splats. Calling the asterisk a splat has a history in computing. In Commodore file systems, an asterisk appearing next to a
filename was called a "splat file" 1.
The splat operator is the *
, think of it like saying etc when writing code. In Ruby, a splat is a unary operator that explodes an array (or an object that implements to_a
) and returns an array.
This definition might seem kind of heady, so I'm going to bring it back down to earth with some concrete examples. First, we will start with some simple use cases to explore what's going on, and then we will dig into some
Ruby code that takes advantage of the splat.
Splats aren't exclusive to Ruby, but this post's scope only covers using them in Ruby.
Expected behaviors
Splats have a few different expected behaviors. We will explore how to use them to do various things and look at test cases from the Ruby language spec. The examples below are purely for illustrative purposes and are not necessarily examples of sophisticated code architecture.
Creating a method
An expected splat behavior is to create a method that accepts a variable number of arguments. Everything passed to the splatted parameter is collected inside an array with the same name as the parameter.
def example(a, *args)
puts a
puts args.inspect
end
example("a", "b", "c", "d")
a
["b", "c", "d"]
The rest of the array is now available inside of args
.
The arguments you pass don't all have to be the same type either:
def example(a, *args)
puts a
puts args.inspect
end
example("a", 1, true, [1, 2, 3])
a
[1, true, [1, 2, 3]]
Splats can go anywhere
The most common place to put a splat is at the end of the parameter list; after all, it does act as the etc. But the splatted parameter can go anywhere.
def example(a, *args, c)
puts a
puts args.inspect
puts c
end
example("a", 1, true, [1, 2, 3], "Hello")
a
[1, true, [1, 2, 3]]
Hello
While a splat can anywhere, it can't go everywhere. The limit is one splat per method argument list because how would Ruby know where one ends, and the other begins?
def example(*a, *b)
puts a.inspect
puts b.inspect
end
example("a", "b", "c", "d")
SyntaxError: unexpected *
def example(*a, *b)
^
Invoking a method
The above example is how to use a splat when defining a method. We can do the inverse and use a splat when invoking a method, too. We have a function that expects three arguments, name
, age
, favorite_color
, and an array of triples (an array containing three items), but we don't want to destructure the triples and then pass them into the method. Splat is the operator to handle this, but first, let's look at how we might accomplish what we want without a splat:
def user_info(name, age, favorite_color)
puts "#{name} is #{age} years old and their favorite color is #{favorite_color}"
end
users = [
["Meagan", 28, "pink"],
["Lauren", 25, "purple"],
["Bob", 32, "green"],
["Leonard", 7, "blue"]
]
users.each { |user| user_info(user[0], user[1], user[2]) }
Meagan is 28 years old and their favorite color is pink
Lauren is 25 years old and their favorite color is purple
Bob is 32 years old and their favorite color is green
Leonard is 7 years old and their favorite color is blue
A splat removes the need for the explicit destructuring step:
def user_info(name, age, favorite_color)
puts "#{name} is #{age} years old and their favorite color is #{favorite_color}"
end
users = [
["Meagan", 28, "pink"],
["Lauren", 25, "purple"],
["Bob", 32, "green"],
["Leonard", 7, "blue"]
]
users.each { |user| user_info(*user) }
Meagan is 28 years old and their favorite color is pink
Lauren is 25 years old and their favorite color is purple
Bob is 32 years old and their favorite color is green
Leonard is 7 years old and their favorite color is blue
Array Destructuring
I hinted at it above, but what the splat is doing is destructuring arrays. When you break an array down into individual elements, that is destructuring.
name, age, favorite_color = ["Meagan", 28, "pink"]
puts name
#=> Meagan
puts age
#=> 28
puts favorite_color
#=> pink
Instead of naming every element in the array with a variable, we can use the splat operator.
Pop & Shift
The .pop
and .shift
method on Array
in Ruby mutate the array they are invoked on. For example:
arr = [1, 2, 3, 4]
arr.shift
#=> 1
arr
#=> [2, 3, 4]
arr = [1, 2, 3, 4]
arr.pop
#=> 4
arr
#=> [1, 2, 3]
What if I want the first element, but I want to leave the array the way I found it?
arr = [1, 2, 3, 4]
first, *rest = arr
first
#=> 1
rest
#=> [2, 3, 4]
arr
#=> [1, 2, 3, 4]
If I just want the last element the splat goes at the beginning:
arr = [1, 2, 3, 4]
*first, last = arr
last
#=> 4
first
#=> [1, 2, 3]
arr
#=> [1, 2, 3, 4]
In either example, leave the splat unnamed if you only care about grabbing the first or last element.
arr = [1, 2, 3, 4]
first, * = arr
first
#=> 1
arr = [1, 2, 3, 4]
*first, last = arr
last
#=> 4
Other Array Use cases
You can also use a splat to flatten an array.
names = ["Meagan", "Lauren", "Leonard", "Bob"]
more_names = ["Alice", "John", *names, "Mary"]
more_names
=> ["Alice", "John", "Meagan", "Lauren", "Leonard", "Bob", "Mary"]
And you can use a splat to make an array.
arr = *"example"
#=> ["example"]
hsh ={a: "a", b: "b"}
splatted = *hsh
#=> [[:a, "a"], [:b, "b"]]
Ruby language spec examples
The Ruby language spec is a great place to look to see how something works. Let's look at a few test cases that explain what a splat is doing. For the examples below, I'll be referencing this file. I'm not going to get too into the how of these tests or their setup. Please reference the documentation to learn more. It's a cool project.
context "with a single splatted Object argument" do
before :all do
def m(a) a end
end
it "does not call #to_ary" do
x = mock("splat argument")
x.should_not_receive(:to_ary)
m(*x).should equal(x)
end
it "calls #to_a" do
x = mock("splat argument")
x.should_receive(:to_a).and_return([1])
m(*x).should == 1
end
end
These tests show that x
(the splatted argument) is turned into an array, by invoking .to_a
on it (the should receive and return) when it is sent as a splatted argument to the m
method (defined in the before :all
block). Below is a test
that verifies the behavior we saw in the section "Invoking a method."
What would you expect this method invocation to return?
def m(a, b, *c, d, e)
[a, b, c, d, e]
end
x = [1]
m(*x, 2, 3, 4)
Hold that expectation in your head and let's look at what the corresponding test says:
context "with a leading splatted Object argument" do
before :all do
def m(a, b, *c, d, e) [a, b, c, d, e] end
end
it "calls #to_a" do
x = mock("splat argument")
x.should_receive(:to_a).and_return([1])
m(*x, 2, 3, 4).should == [1, 2, [], 3, 4]
end
...
end
c
is turned into an empty array even though it came in the middle of the parameter list. Recall what we learned above, a splat can go anywhere in the parameter list, and Ruby is smart about resolving it. Because there are four
required arguments, *c
doesn't gather up any of them into an array. There is no etc or rest.
What would happen if we invoked m
with these arguments?
m(*x, 2, 3, 4, 5, 6, 7, 8, 9)
#=> [1, 2, [3, 4, 5, 6, 7], 8, 9]
Ruby knew it needed the last two arguments to assign to d
and e
. All the rest get shoveled into the array set to c
.
I like to use the Ruby spec in tandem with the Ruby documentation. It's great to read about how something works, and reading test cases helps fill in my mental model even more.
Why would I use it?
You would use splat for a variety of reasons. If we look back at the Array Destructuring section, we can see how powerful it can be for building arrays without the need to coerce our types manually.
For some hash where there's a key of foo
that has a value of some array, a typical use case sans splatting might look like:
- Checking that the array exists, and creating it if not
- Work regardless of someone passing in an array of strings or a single string
With splatting, that workflow looks like this:
hsh = {}
def add_foo
["bar", "baz"]
end
hsh[:foo] = [*hsh[:foo], *add_foo]
hsh
#=> {:foo=>["bar", "baz"]}
hsh = {}
hsh[:foo] = [*hsh[:foo], *"bar"]
#=> {:foo=>["bar"]}
Real-World Examples
The sinatra gem uses splat args multiple times in their codebase. Here's one example from their test suite
def use(*args)
@use << args
end
before do
@use = []
end
We don't need to get into the nitty-gritty of how they're using it in this particular use case. But we can see that the tests are setting up an empty array called @use
, and they want to fill that array with elements, but they don't know how many, so use
takes advantage of the splatted parameter.
Let's also look at thoughtbot's upcase source code. They're again using a splatted parameter in the test setup, and they're also invoking a method with a splatted argument.
it "doesn't render a CTA link" do
render_trail completeables: []
...
end
# Creating a method with a splatted parameter
def render_trail(*trail_args)
...
# Invoking a method with a splatted argument
...
create_trail(*trail_args)
end
def create_trail(completeables:)
...
completeables.each do |completeable|
...
end
...
end
In the above code, first render_trail
is invoked with completeables: []
. trail_args
is a splatted parameter; if we were to inspect
it, it would look like [{completeables: []}]
. Next, we invoke create_trail
with a splatted argument, meaning we explode out trail_args
, and pass completeables: []
to create_trail
.
These are just two examples I quickly found while looking through repositories I found. There are so many examples of libraries using splats to accomplish so many cool things.
Conclusion & Further Reading
I hope that splats are more approachable after reading this post and seeing the examples. This post is only a jumping-off point, and there is a lot more you can do and tons of ways that Rubyists use splats, including lots of compelling use cases in popular libraries like Rails. Take a look around in your favorite Ruby codebases, and let me know if you see any examples. The links below go further into what you can do with splats or provide more information.
While this was a pretty comprehensive overview of the splat, we haven't even scratched the surface of the double-splat yet; if you're interested in a blog post about that, please let me know by sending me a tweet.
- What is the standalone splat operator(*) used for in Ruby?
- Naked asterisk parameters in Ruby
- The Ruby method spec
- The Strange Ruby Splat
- Fun with keyword arguments
Top comments (2)
Hash rocket? Really?
Indeed! Kind of a silly name, but frequently used by Rubyists to refer to the fat arrow syntax.