Re: What is the splat operator doing here?
I’ve received an email from a fellow software developer inquiring about a bit of code where I use Ruby’s splat operator in two different ways. In my Ruby gem cards_lib I wrote a “macro” to take a list of strings and generate card instances from them. You don’t really hear of macros in the Ruby community but by the definition this is just that.
A macro (short for “macroinstruction”, from Greek μακρός ‘long’) in computer science is a rule or pattern that specifies how a certain input sequence (often a sequence of characters) should be mapped to a replacement output sequence (also often a sequence of characters) according to a defined procedure.
The macro I designed is this:
Cards = ->*c{ c.map {|*opts| Card.new(*opts.flatten)} }
Now the splat operator works in two different ways. One is for consuming all parameters in as one array variable, and the other is to consume all entries out of one array as parameters.
Splat in a Method Definition
Let look at the first situation “consuming all parameters in as one array”. Whether you use a proc/lamda or a method the behavior will be the same. No matter how many parameters given will be evaluated as on inner variable as an array of each of the parameters.
def exampleA *input print "The variable 'input' is an '", input.class, "' which is the following: ", input.to_s end exampleA 1 # The variable 'input' is an 'Array' which is the following: [1] exampleB = ->*input{ print "The variable 'input' is an '", input.class, "' which is the following: ", input.to_s } exampleB.call(1) # The variable 'input' is an 'Array' which is the following: [1] def exampleC input print "The variable 'input' is an '", input.class, "' which is the following: ", input.to_s end exampleC 1 # The variable 'input' is an 'Integer' which is the following: 1 exampleD = ->input{ print "The variable 'input' is an '", input.class, "' which is the following: ", input.to_s } exampleD.call(1) # The variable 'input' is an 'Integer' which is the following: 1
The splat operator as a parameter definition will consume any input into one array.
def exampleE *input puts input.to_s end exampleE # [] exampleE 1, 2, 3, [4] # [1, 2, 3, [4]]
Splat in a Method Call
Using splat in a method call is a bit like unwrapping one layer from an array. It’s the same as saying you want to take all of the individual entries in the array and pass them off as individual parameters to the method.
def exampleF a, b=nil, c=nil, *x print [a, a.class], "\n" print [b, b.class], "\n" print [c, c.class], "\n" print [x, x.class], "\n" end exampleF 1, 2, 3, 4, 5, 6, 7 # [1, Integer] # [2, Integer] # [3, Integer] # [[4, 5, 6, 7], Array] exampleF 1 # [1, Integer] # [nil, NilClass] # [nil, NilClass] # [[], Array] # SPLAT IN METHOD CALL EXAMPLE array = [:a, :b, :c, :d, :e] exampleF *array # [:a, Symbol] # [:b, Symbol] # [:c, Symbol] # [[:d, :e], Array]
So as you can see the splat parameter is a way of wrapping or unwrapping an array as one or many parameters.
Going Through the Macro
Cards = ->*c{ c.map {|*opts| Card.new(*opts.flatten)} }
The first thing this lambda Proc does is take all parameters in as an Array.
Cards = ->*c{ puts "c = #{c.to_s}" } Cards.call("Th", "Jh", "Qh", "Kh", "Ah") # c = ["Th", "Jh", "Qh", "Kh", "Ah"]
If we had the collection as an array beforehand we’d splat it in and get the same results.
Cards = ->*c{ puts "c = #{c.to_s}" } cards = ["Th", "Jh", "Qh", "Kh", "Ah"] Cards.call(*cards) # c = ["Th", "Jh", "Qh", "Kh", "Ah"]
Now that we have any quantity of cards provided as an array we’ll map them into Card objects. Let’s look at the map part.
c.map {|*opts| Card.new(*opts.flatten)}
This will replace all the string representations of a card and return a Card object for each.
Just so you know; the Card object is defined elsewhere and can take two parameters:
class Card def initialize(face, ranker = Ranker)
Now the mapping process takes in each entry from c and the splat operator on *opts wraps that entry in an array. So if the first item is a string such as “Ah” it will now be [“Ah”]. Now that we have a guaranteed array we can call the flatten method with absolute confidence that we wont get a missing method error (and flatten is needed if arrays are given since the incoming splat operator will double the array depth). Now since the Card object can take two individual parameters we need to use the splat operator again to “unwrap” the array as parameters to the new method.
Now to make clear the WHY behind doing all this. As you can see from the class Card definition you can define your own ranker object on a card. With the above macro you could pass in the following.
Cards.call(["Ah", PokerRanker], ["2h", BlackjackRanker])
And each Card object would be built successfully with a different internal ranker given as a parameter. This way the mapper in this macro doesn’t care if you give it a string for a card, or an array of parameters, it will be flattened and then splatted out just the same to Card.new. So the macro accepts a simple list, or a complex one for Card generation.
Style Notes
When using lambdas or procs I tend to hear a preference towards using .call or .() in providing clarity for when a Proc being called rather than using the [] method which is the same thing but can be confused as a hash. For the sake of teaching in this post I used .call, but for me personally since a macro isn’t a common term thrown around the Ruby community I like to use the square brackets for this and think of it as “macro syntax”. So making five cards would simply be Cards[ “Ah”, “2h”, “3h”, “4h”, “5h” ]. So when you examine the code for CardsLib you may find it helpful to keep that in mind.
Summary
The splat operator is super helpful but it can only be used in a method definitions or in method calls. If you try to use the splat operator inside of string interpolation it won’t work for unwrapping an array as it did in method calls. So it has a good use case, but a limited one.
If you haven’t checked out my gem CardsLib I highly recommend that you do. I boast of it as “The ‘smartest’ object oriented card game library”. It has many efficient and helpful ways of creating and evaluating cards, and collections of cards, and their worth. It may just be helpful enough in making your favorite card games an easy program to make.
As always I hope you found this educational and enjoyable! Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!
God Bless!
-Daniel P. Clark
Image by Jesse Kruger via the Creative Commons Attribution-NonCommercial 2.0 Generic License
jfnixon
January 24, 2017 - 1:49 pm
Nice explanation with a good example.
Daniel P. Clark
January 25, 2017 - 11:23 pm
Thanks!