A PlanarFe Adventure

LearnLoveCode

Keyword Arguments

During a morning standup (less fun than actual standup) at work someone mentioned that a method was a good candidate for a refactor using Keyword Arguments. So I went back to my desk, googled it, and my newb brain said ‘Why?’.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def some_method(arg1, arg2, arg3)
  puts arg1 + arg2 + arg3
end

some_method("a", "b", "c") #=> "abc"
some_method #=> ArgumentError: wrong number of arguments (0 for 3)


def method_with_keyword_args(arg1:, arg2:, arg3:)
  puts arg1 + arg2 + arg3
end

method_with_keyword_args(arg1: "a", arg2: "b", arg3: "c") #=> "abc"
method_with_keyword_args #=>ArgumentError: missing keywords: arg1, arg2, arg3

So above we’re looking at a method defined with your typical (positional) arguments and another method using required keyword arguments (defined by the colon following the argument name). It just looked like more typing to me. Maybe with a better error, so ok, there’s a point for that I guess.

Default arguments are defined just as you might expect:

1
2
3
4
5
6
7
8
9
10
def keyword_args(arg1:"a", arg2:"b", arg3:"c")
  puts arg1+arg2+arg3
end

keyword_args #=> "abc"
keyword_args(arg1:"Hello", arg2:"World", arg3:"!") #=> "HelloWorld!"

def positional_args(arg1="a", arg2="b", arg3="c")
  puts arg1+arg2+arg3
end

But still, is there really much of an advantage? Readability is one way that keyword arguments might be a good way to go rather than positional arguments. If you were to encounter a method call in your codebase the keywords give you a clue as to what the arguments mean and maybe even how the argument are used within the method. Positional arguments would require you to go look up the method definition.

1
2
3
4
# calling a method with positional arguments
electrical_power(12,2) #=> 72
# calling a method with keyword arguments
electrical_power(volts: 12, ohms: 2) #=> 72

Beyond that, since in your are referencing the variable name contained in your method from your method call and assigning it a value for that method call, the order of the arguments does not matter.

1
2
3
4
5
6
7
# calling a method with positional arguments
electrical_power(12,2.0) #=> 72
electrical_power(2.0,12) #=> 0.3333333333333333

# calling a method with keyword arguments
electrical_power(volts: 12, ohms: 2.0) #=> 72
electrical_power(ohms: 2.0, volts: 12) #=> 72

To do something similar without keyword arguments would require passing a hash as an argument:

1
2
3
4
5
6
def electrical_power(inputs)
  inputs[:volt]**2 / inputs[:ohms]
end

electrical_power({volts:12, ohms:2}) #=> 72
electrical_power #=> ArgumentError: wrong number of arguments (0 for 1)

Giving that method default arguments might look something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
def electrical_power(inputs={volts: 12, ohms: 2})
  inputs[:volts]**2 / inputs[:ohms]
end

# OR

def electrical_power(inputs={})
  inputs.fetch(:volts,12)**2 / inputs.fetch(:ohms, 2)
end


electrical_power({ohms:4 ,volts:8}) #=>16
electrical_power #=> 72

So why not just use keyword arguments at that point and skip the curly braces, square brackets, and ‘.fetch’-es?

What really sold me is this:

When one Ruby method has to know the correct order of another method’s positional arguments, we end up with connascence of position.

If we decide to change the order of the parameters to mysterious_total, we must change all callers of that method accordingly. Not only that, but our mental model of how to use this method must change as well, which isn’t as simple as a find/replace.

This statement gave me two thoughts:

1. I haven’t been programming for long, but I’ve learned enough to know that it’s easier to build a clean interface once than to modify every call to a method when its implementation changes. When, not if.

2. More than that, if you are calling a method from another method or interacting with another object, with positional arguments that method or object now needs to know something about the order of arguments passed to the original method. It’s like it gets a little peak into the internal of the first method which doesn’t feel very encapsulated to me.

Happy Halloween!!!

1
2
3
4
5
6
7
8
9
10
11
12
def spooky_method(who:,what:,where:)
  puts "#{who} did the #{what}!"
  puts "He did the monster #{what}."
  puts "It was a smash."
  puts "It was a #{where} smash."
end

spooky_method(who: "Frankenstein", what: "Mash", where:"Graveyard")
  #=> "Frankenstein did the Mash!"
  #=> "He did the monster Mash."
  #=> "It was a smash."
  #=> "It was a Graveyard smash."

Sources:

“Ruby 2 Keyword Arguments” - Ian C. Anderson (Thoughtbot)