In Progress
Unit 1, Lesson 21
In Progress

More Keyword Arguments

Video transcript & code

In the last episode we converted a method from using old-style pseudo-keywords to real Ruby 2.1 keyword arguments. There's not a lot left to know about keyword arguments, but today we'll cover one or two more advanced use cases.

Here's the burger-ordering method we defined in the last episode. It takes a customer name and a series of keyword arguments specifying options and condiments.

def order_burger(customer_name, patty: :beef, cheese:, onions: true, bacon: false)
  toppings = []
  toppings << :onions if onions
  toppings << :bacon  if bacon

  print "That's one #{patty} #{cheese ? "cheese" : ""}burger"
  print " with #{toppings.join(', ')}"
  print " for #{customer_name}.\n"
end

Let's say we have a new method that calls our order_burger method, called order_meal. It adds a few extra options for whether the customer wants a side of fries and what type of drink they want.

Notice that this method has yet to be upgraded to keyword arguments. Let's try calling it.

require "./order_burger"
def order_meal(customer_name, options={})
  has_fries = options.delete(:fries) { true }
  drink     = options.delete(:drink) { "cola" }
  order_burger(customer_name, options)
  puts "With a #{drink}#{has_fries ? ' and fries' : ''}."
end

order_meal("Grimm", drink: "root beer", cheese: true, bacon: true)
# >> That's one beef cheeseburger with onions, bacon for Grimm.
# >> With a root beer and fries.

As we can see, despite the fact that this method passes an old-style options hash to order_burger, it Just Works. Ruby figures out that we want to treat that options hash as keyword arguments. This is one of my favorite things about the new keyword arguments: they are completely backward-compatible so that we can incrementally update a codebase to use them bit by bit.

Now let's say we want to update this method to use keyword arguments. For the :fries and :drink keywords this is straightforward enough - we go through the same process we did in the last episode with order_burger.

But what about the options that the methods passe through to #order_burger? Do we have to also include keyword specifications for each of them?

Thankfully we do not. Instead, we use the new "double star" operator to tell Ruby: "collect all other keyword arguments into a hash and put them in this variable". Then we pass that argument on to #order_burger.

require "./order_burger"
def order_meal(customer_name, fries: true, drink: "cola", **options)
  order_burger(customer_name, options)
  puts "With a #{drink}#{fries ? ' and fries' : ''}."
end

order_meal("Grimm", drink: "root beer", cheese: true, bacon: true)
# >> That's one beef cheeseburger with onions, bacon for Grimm.
# >> With a root beer and fries.

Now let's look at another method. This one is a thin wrapper around #order_burger. All it does is force the cheese option to true.

require "./order_burger"

def order_cheeseburger(customer, options={})
  options[:cheese] = true
  order_burger(customer, options)
end

order_cheeseburger("Grimm")
# >> That's one beef cheeseburger with onions for Grimm.

Let's update this method to use keyword arguments. We use the double star again to collect keyword args into an options hash. We could just leave it at that. But there's another possibility. Instead of having a line of code to update the options hash, we can specify it as a keyword argument directly to #order_cheeseburger. Then we can use the double star on the options hash. This effectively "splats" the options hash out, combining it with any other keywords arguments that are explicitly supplied into one big keyword argument list.

require "./order_burger"

def order_cheeseburger(customer, **options)
  order_burger(customer, cheese: true, **options)
end

order_cheeseburger("Grimm")
# >> That's one beef cheeseburger with onions for Grimm.

Now let's look at yet another method. This method handles orders placed at the drive-through. It doesn't add any new options of its own.

require "./order_burger"

def order_burger_drivethrough(customer, options={})
  order_burger(customer, options)
  puts "Thank you, please pull around!"
end

order_burger_drivethrough("Grimm", cheese: true, bacon: true)
# >> That's one beef cheeseburger with onions, bacon for Grimm.
# >> Thank you, please pull around!

We might question whether it's worthwhile to update a method like this, since it works fine and it's already pretty concise. But consider this: what if someone accidentally supplies something other than a hash for the options parameter.

require "./order_burger"

def order_burger_drivethrough(customer, options={})
  order_burger(customer, options)
  puts "Thank you, please pull around!"
end

order_burger_drivethrough("Grimm", nil)
# ~> -:4:in `order_burger_drivethrough': missing keyword: cheese (ArgumentError)
# ~>    from -:8:in `<main>'

This error comes from the call to #order_burger, inside of #order_burger_drivethrough.

Now let's change the options to be a keyword list using the double star.

require "./order_burger"

def order_burger_drivethrough(customer, **options)
  order_burger(customer, options)
  puts "Thank you, please pull around!"
end

order_burger_drivethrough("Grimm", nil)
# ~> -:3:in `order_burger_drivethrough': wrong number of arguments (2 for 1) (ArgumentError)
# ~>    from -:8:in `<main>'

This error comes from an earlier point in the code, telling us that #order_burger_drivethrough expected only one positional argument, but it received two. By telling Ruby we expect keyword arguments, it is able to do a little extra argument checking, verifying that keyword arguments are in fact supplied where they are expected.

OK! With that, you should know everything you need to make effective use of keyword arguments in Ruby 2.1. I hope you enjoy transitioning to using them as much as I have. Happy hacking!

Responses