Fun With Keyword Arguments, Hashes, and Splats

You’ve probably seen this pattern before. A method has an options hash as its last argument, which holds extra parameters:

def hello_message(name_parts = {})
  first_name = name_parts.fetch(:first_name)
  last_name = name_parts.fetch(:last_name)

  "Hello, #{first_name} #{last_name}"
end

Unfortunately, you need to extract those parameters out of the hash. And that means there’s a lot of setup to wade through before you get to the good part.

But if you changed this method to use keyword arguments in Ruby 2.0+, you wouldn’t have to pull :first_name and :last_name out of your hash. Ruby does it for you:

def hello_message(first_name:, last_name:)
  "Hello, #{first_name} #{last_name}"
end

Even better, if your app uses Ruby 1.9+ hash syntax, your methods can use keyword arguments without changing those methods’ callers:

def hello_message_with_an_options_hash(name_parts = {})
  first_name = name_parts.fetch(:first_name)
  last_name = name_parts.fetch(:last_name)

  "Hello, #{first_name} #{last_name}"
end

def hello_message_with_keyword_arguments(first_name:, last_name:)
  "Hello, #{first_name} #{last_name}"
end

hello_message_with_an_options_hash(first_name: "Justin", last_name: "Weiss")

hello_message_with_keyword_arguments(first_name: "Justin", last_name: "Weiss")

See? Those arguments are identical!

Pushing keyword argument syntax one step too far

What if you haven’t switched to the new Hash syntax, though? You could convert all your code. But, at least in Ruby 2.2.1, the old hash syntax works just fine with keyword arguments:

irb(main):007:0> hello_message_with_keyword_arguments(:first_name => "Justin", :last_name => "Weiss")
=> "Hello, Justin Weiss"

Nice! What about passing a hash object, instead of arguments?

irb(main):008:0> options = {:first_name => "Justin", :last_name => "Weiss"}
irb(main):009:0> hello_message_with_keyword_arguments(options)
=> "Hello, Justin Weiss"

Whoa. What if we want to mix a hash and keyword arguments?

irb(main):010:0> options = {last_name: "Weiss"}
irb(main):011:0> hello_message_with_keyword_arguments(first_name: "Justin", options)
SyntaxError: (irb):11: syntax error, unexpected ')', expecting =>
	from /usr/local/bin/irb:11:in `<main>'

OK. I guess we took that one step too far. To fix this, you could use Hash#merge to build a hash you could pass in on its own. But there’s a better way.

If you were using regular arguments instead of keyword arguments, you could splat arguments from an Array, using *:

def generate_thumbnail(name, width, height)
  # ...
end

dimensions = [240, 320]
generate_thumbnail("headshot.jpg", *dimensions)

But is there a way to splat keyword arguments into an argument list?

It turns out there is: **. Here’s how you’d fix that broken example with **:

irb(main):010:0> options = {last_name: "Weiss"}
irb(main):011:0> hello_message_with_keyword_arguments(first_name: "Justin", **options)
=> "Hello, Justin Weiss"

And if you’re really crazy, you can mix regular arguments, keyword arguments, and splats:

def hello_message(greeting, time_of_day, first_name:, last_name:)
  "#{greeting} #{time_of_day}, #{first_name} #{last_name}!"
end

args = ["Morning"]
keyword_args = {last_name: "Weiss"}

hello_message("Good", *args, first_name: "Justin", **keyword_args) # => "Good Morning, Justin Weiss!"

Of course, if you find yourself in the situation where that’s necessary, you probably made a mistake a lot earlier!

Capture keyword arguments the easy way

Do you know how you can turn all your method arguments into an array using *?

def argument_capturing_method(*args)
  args
end

argument_capturing_method(1, 2, 3) # => [1, 2, 3]

This also works with keyword arguments. They’re converted to a hash, and show up as the last argument of your args array:

argument_capturing_method(1, 2, 3, key: "value") # => [1, 2, 3, {:key=>"value"}]

But args.last[:key] isn’t the best way to read keyword arguments grabbed this way. Instead, you can use the new ** syntax to get the keyword arguments by themselves:

def dual_argument_capturing_method(*args, **keyword_args)
  {args: args, keyword_args: keyword_args}
end

dual_argument_capturing_method(1, 2, 3, key: "value") # => {:args=>[1, 2, 3], :keyword_args=>{:key=>"value"}}

With this syntax, you can access the first regular argument with args[0] and the :key keyword argument with keyword_args[:key].

… Of course, now we’re back to options hashes.


Keyword arguments are great for removing a ton of parameter extraction boilerplate from your code. And you might not even have to change any of your code to take advantage of them.

But when you write more generic methods, there are some new techniques you’ll have to learn to handle keyword arguments well. You might not have to use those techniques very often. But when you do, this power and flexibility will be there, waiting for you.

Pushing through tutorials, and still not learning anything?

Have you slogged through the same guide three times and still don't know how to build a real app?

In this free 7-day Rails course, you'll learn specific steps to start your own Rails apps — without giving up, and without being overwhelmed.

You'll also discover the fastest way to learn new Rails features with your 32-page sample of Practicing Rails: Learn Rails Without Being Overwhelmed.

Sign up below to get started:

Powered by ConvertKit

Did you like this article? You should read these:

Comments