The Splat Operator

Meagan Waller

XXX words · XX min read

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](https://gist.github.com/meaganewaller/515e4c6e20a6d3d27cea909f45cda4df). ## 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. ```ruby def example(a, *args) puts a puts args.inspect end example("a", "b", "c", "d") ``` ```text 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: ```ruby def example(a, *args) puts a puts args.inspect end example("a", 1, true, [1, 2, 3]) ``` ```text 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. ```ruby def example(a, *args, c) puts a puts args.inspect puts c end example("a", 1, true, [1, 2, 3], "Hello") ``` ```text 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? ```ruby def example(*a, *b) puts a.inspect puts b.inspect end example("a", "b", "c", "d") ``` ```text 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: ```ruby 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]) } ``` ```text 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: ```ruby 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) } ``` ```text 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. ```ruby 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: ```ruby arr = [1, 2, 3, 4] arr.shift #=> 1 arr #=> [2, 3, 4] ``` ```ruby 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? ```ruby 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: ```ruby 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. ```ruby arr = [1, 2, 3, 4] first, * = arr first #=> 1 ``` ```ruby arr = [1, 2, 3, 4] *first, last = arr last #=> 4 ``` ### Other Array Use cases You can also use a splat to flatten an array. ```ruby names = ["Meagan", "Lauren", "Leonard", "Bob"] more_names = ["Alice", "John", *names, "Mary"] more_names ``` ```text => ["Alice", "John", "Meagan", "Lauren", "Leonard", "Bob", "Mary"] ``` And you can use a splat to make an array. ```ruby arr = *"example" #=> ["example"] hsh ={a: "a", b: "b"} splatted = *hsh #=> [[:a, "a"], [:b, "b"]] ``` ### Ruby language spec examples The [Ruby language spec](https://github.com/ruby/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](https://github.com/ruby/spec/blob/master/language/method_spec.rb). 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. ```ruby 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? ```ruby 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: ```ruby 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? ```ruby m(*x, 2, 3, 4, 5, 6, 7, 8, 9) ``` ```text #=> [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: 1. Checking that the array exists, and creating it if not 2. Work regardless of someone passing in an array of strings or a single string With splatting, that workflow looks like this: ```ruby 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](https://github.com/sinatra/sinatra) gem uses splat args multiple times in their codebase. Here's one example from their [test suite](https://github.com/sinatra/sinatra/blob/8dc1e4ab2bec6be522ed2f700771ccf16a66c3c2/sinatra-contrib/spec/streaming_spec.rb#L16-L18) ```ruby 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](https://github.com/thoughtbot/upcase/blob/bb787c70d3182f458016a7d51d483bf70d932ad9/spec/views/trails/_incomplete_trail.html.erb_spec.rb#L15-L35). They're again using a splatted parameter in the test setup, and they're also invoking a method with a splatted argument. ```ruby 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?](https://stackoverflow.com/questions/42158005/what-is-the-standalone-splat-operator-used-for-in-ruby) - [Naked asterisk parameters in Ruby](http://andrewberls.com/blog/post/naked-asterisk-parameters-in-ruby) - [The Ruby method spec](https://github.com/ruby/spec/blob/master/language/method_spec.rb) - [The Strange Ruby Splat](https://endofline.wordpress.com/2011/01/21/the-strange-ruby-splat/) - [Fun with keyword arguments](https://www.justinweiss.com/articles/fun-with-keyword-arguments/) [^1]: [Asterisk wikipedia page](https://en.wikipedia.org/wiki/Asterisk) [^1]: [Asterisk wikipedia page](https://en.wikipedia.org/wiki/Asterisk)

On this page