With Ruby 2.0 we now have first-class support for keyword arguments in our method parameters. (“First class” means it is a fundamental part of the language and doesn’t require extensions to make work.)
def foo(name: 'default')
puts name
end
foo # => 'default'
foo(name: 'something') # => 'something'
In Ruby 1.9, we could do something similar using a single Hash parameter:
def foo(options = {})
name = options.fetch(:name, 'default')
puts name
end
foo # => 'default'
foo(name: 'something') # => 'something'
Ruby 2.0 blocks can also be defined with keyword arguments:
define_method(:foo) do |name: 'default'|
puts name
end
foo # => 'default'
foo(name: 'something') # => 'something'
To achieve a similar behavior in Ruby 1.9, the block would have had to take an options hash, from which we would then extract argument values.
Required keyword arguments
Ruby 2.1 introduced required keyword arguments, which are defined with a trailing colon. (Although keyword arguments are available in Ruby 2.0, it wasn’t until Ruby 2.1 that you could specify required and non-requir)
def foo(name:)
puts name
end
foo # => ArgumentError: missing keyword: bar
foo(name: 'something') # => 'something'
If a required keyword argument is missing, Ruby will raise an ArgumentError
which tells us specifically which required argument the calling code forgot to include.
Keyword arguments vs. options hash
With keyword arguments defined in the method signature itself, we can immediately discover the names of the arguments without having to read the body of the method.
With “first-class” keyword arguments now in Ruby, we don’t have to write the “boilerplate” code to extract hash options.
Note that “the calling code” is syntactically equal to calling a method with hash arguments. This makes for an easy transition from options hashes (pre Ruby 2.0) to keyword arguments (Ruby 2.0).
Keyword arguments vs positional arguments
Assume we have a method with positional arguments:
def strange_total(subtotal, tax, discount)
subtotal + tax - discount
end
strange_total(200, 50, 5) # => 245
This method does its job, but as the next coder who comes across strange_total
, the arity (positional arguments) of the arguments makes it impossible to read right away what the numbers 200
, 50
, and 5
signify.
By using keyword arguments, the next developer will know what the arguments mean without looking up the implementation of the called method.
def obvious_total(subtotal:, tax:, discount:)
subtotal + tax - discount
end
obvious_total(subtotal: 200, tax: 50, discount: 5) # => 245
Keyword arguments allow us to switch the order of the arguments. When we do, we do not affect the behavior of the method:
obvious_total(subtotal: 100, discount: 5, tax: 10) # => 145
If we switch the order of the positional arguments, we are not going to get the same results.
mysterious_total(200, 5, 50) # => 155
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.