Jason Fleetwood-Boldt
Posted on June 27, 2023
originally published https://jasonfleetwoodboldt.com/courses/stepping-up-rails/ruby-arguments-equal-signs-colons-splats-double-splats-explained/
Watch the companion video on Youtube
Ruby has two basic types of arguments: positional arguments, which are specified by arity — that is, by the positioning of the variable in the method definition, and keyword arguments, which are specified by the name of the argument.
Most programmers are familiar with the basic concept of arity-based parameters: The first variable passed to the method becomes the first variable within the code of the executing method. These are called arity-based parameters.
Keyword arguments, introduced in Ruby 2.0, allow you to write code that doesn’t depend on the positioning of the arguments because every argument is explicitly named.
Positional Arguments
I will also refer to positional arguments as “arity parameters” but the formal name is positional arguments. Whatever it is called, it has a basic structure known to all programmers: the first parameter passed becomes the first variable, the second becomes the second, etc. The order you pass the variables into the calling code determine what variables get which vales inside the method.
*Example 1: Basic Positional Arguments are Ordered
*
def my_method(apple, banana, cherry)
"#{cherry}#{banana}#{apple}"
end
puts my_method(1,2,3)
# 3 2 1
puts my_method(1,2)
# missing argument - illegal in Ruby
Notice that this construction makes apple, banana, and cherry all required. If you want to make them optional, you’ll want to use an = and give them a default value. Because all 3 are required, we must call the above method with three parameters (that’s why the last example is illegal syntax).
Parameter Default Values
Positional arguments will have a default value if specified with an = sign. Whatever you put after the = will be the default value.
*Example 2: Positional Arguments Can Be Optional
*
def my_method(apple = nil , banana = 'missing', cherry = 0)
"apple=#{apple}|banana=#{banana}|cherry=#{cherry}"
end
puts my_method(4, 'found', 3 ) # apple=4|banana=found|cherry=3
puts my_method(4, 'found', nil) # apple=4|banana=found|cherry=
puts my_method(4, 'xyz') # apple=4|banana=xyz|cherry=0
puts my_method(4) # apple=4|banana=missing|cherry=0
puts my_method # apple=|banana=missing|cherry=0
*Example 3: Optional Arguments Should Come Last
*
def my_method(apple, banana, cherry = 0)
"apple=#{apple}|banana=#{banana}|cherry=#{cherry}"
end
#
puts my_method(4, 'found', 3 ) # apple=4|banana=found|cherry=3
puts my_method(4, 'found', nil) # apple=4|banana=found|cherry=
puts my_method(4, 'xyz') # apple=4|banana=xyz|cherry=0
# ILLEGAL RUBY
# my_method(4) # wrong number of arguments (given 1, expected 2..3)
# my_method # wrong number of arguments (given 0, expected 2..3)
Positional Arguments With Default Should Come Last
More commonly used parameters typically come first because you can skip parameters from the right-to-left but not from left to right. thus, the lesser-needed parameters typically come after more used parameters when designing an API
*Example 4: Putting optional arguments first doesn’t make sense
*
def my_method(apple = "a", bannana)
"apple=#{apple}|banana=#{bannana}"
end
puts my_method("b", 3) # apple=b|banana=3
# ILLEGAL
# puts my_method(, 1) # ILLEGAL RUBY
# puts my_method # wrong number of arguments (given 0, expected 1..2)
This demonstrates a useless design: there is no way to use the first parameter’s default value.
Rest argument (Single Splat)
Using a Rest argument is how you tell Ruby to “take the rest of the arguments passed” and turn them all into a single array. In the method definition, you specify one single variable (an array) that will take up any arguments beyond the ones already passed.
A single splat operator will receive arguments as an array to a variable or destructure an array into arguments. You can have only one splat operator in a method definition and it must come last.
A ruby method can take a single ‘rest’ argument, specified using a splat * proceeded by the name of the array to take the input.
Remember, a singular argument (always the last) in your method will group up all of the remaining arguments you pass — let’s call them “unclaimed” by the positioned arity arguments, and turn them into a single array. Ruby will make them into a single array, now named the variable name given after the asterisk (*)
*Example 5: The Rest Argument for Positional Arguments that Come After the Specified Positional Arguments (Single Splat)
*
def my_method(*money_array)
"#{money_array.join(' ')}"
end
puts my_method(4,6,8) # 4 6 8
puts my_method(4,6) # 4 6
puts my_method # (prints nothing)
Here, the money_array takes the rest of the arguments are passed into it and turns them into a Ruby array. It is the only argument, so this example is slightly confusing because all of the arguments are “rest” arguments.
*Example 6: Rest Argument Come After Positional Arguments
*
Here, the first two positional arguments (blend and grain) are required
Ruby will crash if they are not supplied. If you supply any more positional arguments, the remaining arguments are considered the rest of the arguments, and get grouped up into an array that goes into the rest argument (in this case, sugar). In this example, you can call my_method with either 2 or more arguments. Any more than 2 (the 3rd, 4th, etc) will be grouped up into the rest argument sugar.
def my_method(blend, grain, *sugar)
"the sugar was #{sugar.join(' and ')} using #{blend} at #{grain}"
end
puts my_method('amber', 'raw', 56, 13, 4) # the sugar was 56 and 13 and 4 using amber at raw
puts my_method('amber', 'raw', 56) # the sugar was 56 using amber at raw
puts my_method('amber')
# ILLEGAL —–wrong number of arguments (given 1, expected 2+
A ruby method can only take one rest argument and will not compile if you specify more than one
def my_method(blend, grain, *sugar, *salt)
// this is illegal ruby
end
If you pass a hash to a Rest argument, you get an array with a single member (the hash).
The rest arguments will always be put into an array so doing something like this is awkward:
*Example 7: Don’t get confused by trying to pass hash to a rest argument.
*
def my_method(blend, *sugar)
"#{sugar.inspect}"
end
puts my_method("brown", {lark: 2, leper: 5}) # "[{:lark=>2, :leper=>5}]"
You have put the hash into an array with a single member, the hash.
Keyword Arguments
Ruby Keyword arguments, introduced in Ruby 2.0, give Ruby the ability for a parameter to be named. You specify its name in both (1) the method definition and (2) the calling code. You do this using a simple key: value syntax. You are freed from having to keep track of the strict positioning (arity) of your methods and are at liberty to refactor your functions with less complexity to keep in your head.
Wield this power wisely!
What is connascense?
Connascence between two software components A and B means either 1) that you can postulate some change to A that would require B to be changed (or at least carefully checked) in order to preserve overall correctness, or 2) that you can postulate some change that would require both A and B to be changed together in order to preserve overall correctness.
– Meilir Page-Jones, What Every Programmer Should Know About Object-Oriented Design
When one Ruby method has to know the order of another method’s positional arguments, we end up with a connascence of position.
That means we’ll need to change all callers of that method accordingly if we ever change the method arguments (Expensive cost of change.)
In addition, our mental model of how to use this method must change as well.
Keyword arguments have trade-offs. Positional arguments offer a more succinct way to call a method, especially if that method takes only one or two arguments.
The code clarity and maintainability gained from keyword arguments generally outweigh the brevity offered by positional arguments.
If you can easily guess their meanings based on the method’s name and there are only one or two arguments, positional arguments are still fine.
But generally speaking, codebases written after Ruby 2.0 should prefer keyword arguments.
*Example 8: One Keyword Argument
*
def my_method(apple: nil)
"the apple is #{apple}"
end
puts my_method(apple: "mcintosh")
Keyword arguments are reversible. They can come in any order, making them more powerful for refactoring your code in the future. Although it may feel like more typing (more verbose), the payoff for you in the long run is exponential because it removes your coupling to order-based (arity-based) positional arguments.
*Example 9: Keyword Arguments Are Reversible
*
def my_method(apple: nil, banana: nil)
"the apple is #{apple}, the banana is #{banana}"
end
puts my_method(apple: "mcintosh", banana: "yellow")
# the apple is mcintosh, the banana is yellow
puts my_method(banana: "orange", apple: "fiji")
# the apple is fiji, the banana is orange
*Example 10: One Positional Argument Followed by Keyword Arguments
*
def my_method(thing, apple: nil, banana: nil)
"the apple is #{apple}, the banana is #{banana}, but the thing is #{thing}"
end
puts my_method("rock", apple: "mcintosh", banana: "yellow")
# the apple is mcintosh, the banana is yellow, but the thing is rock
puts my_method("hard place", banana: "orange", apple: "fiji")
# the apple is fiji, the banana is orange, but the thing is hard place
puts my_method("rock")
# the apple is , the banana is , but the thing is rock
ILLEGAL RUBY
my_method("rock", "anythin else", apple: "mcintosh", banana: "yellow")
# found extra arguments
my_method("rock", cherry: 'whodis')
# unknown keyword 'cherry'
Keyword Arguments Can have a Default or Be Required (Ruby 2.1)
Introduced with Ruby 2.1, you can specify a keyword that is required by omitting the default.
In the examples above, we specified that if no value was passed, we assumed the value would be nil.
To require a keyword argument — cause Ruby exception to be called by any calling code that does not call the method with the required keyword— specify nothing where you would have specified a default value.
*Example 11: Keyword Arguments Can be Required
*
def my_method(apple: "x", banana: )
"apple is #{apple} and banana is #{banana}"
end
puts my_method(apple: "a", banana: "b")
# my_method(apple: "a") # ILLEGAL: Missing arguments 'banana'
You Can Only Pass a Keyword Argument that is Specified (unless using Double Splat)
Consider for a moment this errant example:
*Example 12: Can’t Use an Unspecified Keyword (without a double splat)
*
def my_method(thing, apple: nil, banana: nil)
"the apple is #{apple}, the banana is #{banana}, but the thing is #{thing}"
end
puts my_method("that", apple: 'golden delicious', phone: 'iphone')
No such keyword specified a keyword phone was defined in the method’s definition.
Double Splat
This is illegal Ruby because the calling code (red) tries to call a keyword argument called phone, but no such keyword argument exists. What if you wanted a way to take the unnamed keyword arguments and group them into a hash? Kind of like the rest arguments for positional arguments, there is a tool for that: The Double Splat **
Prior to Ruby 2.7, you could leave off the double splat and assume that the last argument would be turned into the hash for any unclaimed keyword arguments. However, this behavior is deprecated (no longer used), and was removed in Ruby 3.0. In Ruby 2.7, if you leave off the double splat from the last argument, you’d see a warning message like:
warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
In Ruby 3.0, leaving off the double splat becomes illegal syntax.
In the following example, I’m using the badly named args_hash to store a hash of the unspecified arguments— that is, any keyword arguments which are not apple or banana.
*Example 13: Double Splat Groups Up Unspecified Keyword Args
*
def my_method(thing, apple: nil, banana: nil, **args_hash)
"the apple is #{apple}, the banana is #{banana}, and the additional passed args are #{args_hash.inspect}"
end
puts my_method("that", apple: 'golden delicious', phone: 'iphone')
# the apple is golden delicious, the banana is , and the additional passed args are {:phone=>"iphone"}
puts my_method("this", apple: 'french toast', email: 'noone@nowhere.com')
# the apple is french toast, the banana is , and the additional passed args are {:email=>"noone@nowhere.com"}
puts my_method("the other", apple: 'green tomatoes', phone: 'android', email: 'person@somewhere.com')
# the apple is green tomatoes, the banana is , and the additional passed args are {:phone=>"android", :email=>"person@somewhere.com"}
puts my_method("the other", phone: 'android', apple: 'green tomatoes', email: 'person@somewhere.com')
# the apple is green tomatoes, the banana is , and the additional passed args are {:phone=>"android", :email=>"person@somewhere.com"}
Notice that in the last example above, the calling code comes in a different order from the one before it: phone, apple, email (nothing is required; apple and banana are specified keywords). In both examples, we called the method with keyword arguments phone and email (neither defined on the method), apple came in a different order. But it didn’t change the output: in both cases, apple was picked up as a properly named keyword argument, became first-class variable within the method, and args_hash became any arguments that weren’t specified in the code definition.
You May Only Have One Double Splat and It Must Come as The Last Parameter
Example 14: You can’t specify More Than One Double Splat
**
**ILLEGAL
def my_method(thing, apple: nil, banana: nil, **hello, **goodbye)
"the apple is #{apple}, the banana is #{banana}, and the additional passed args are #{args_hash.inspect}"
end
# Expected block argument or end of arguments list
You can’t have two double splats in a method definition.
Your Double Splat Must Come At the End of the Keyword Arguments
ILLEGAL
def my_method(thing, apple: nil, **hello, banana: nil)
"the apple is #{apple}, the banana is #{banana}, and the additional passed args are #{args_hash.inspect}"
end
You must have the double-splat argument come after the keyword arguments.
*Example 15: Mixing Rest Positional and Rest Keyword is Allowed, but Confusing
*
def my_method(thing, *rest_args, apple: nil, banana: nil, cherry: 0, **hello)
"the thing is #{thing}, apple is #{apple}, banana is #{banana}, chery is #{cherry}
rest positional args are #{rest_args.join(',')} and rest keyword args are #{hello.inspect}"
end
puts my_method("that", "a", "b", "c", "d",
phone: "123-456-7890",
apple: "golden delicious",
email: "person@somewhere.com")
the thing is that, apple is golden delicious, banana is , chery is 0
rest positional args are a,b,c,d and rest keyword args are {:phone=>"123-456-7890", :email=>"person@somewhere.com"}
Posted on June 27, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.