Keyword Arguments - Ruby, Python, Clojure

Keyword Arguments (also called named parameters) are a feature of some programming languages, which gives the programmer the possibility to state the name of the parameter that is set in a function call.
02.04.2019
Andreas Profous
Tags

In this blog post, we’ll explore them in three example languages: Ruby, Python and Clojure.

Ruby

Syntax

In Ruby, keyword arguments are declared using the : syntax:

def words(sentence:)
  sentence.tr(',.!?;', '').split(' '
end
words(sentence: 'I like it!')
# => ['I', 'like', 'it']

This example shows one advantage of using them. The advantage of using this feature is: improved code readability.

Imagine seeing code like:

copy_file(to: 'tmp/foo.txt', from: 'upload/bar.txt')

This is easier to understand than naming the function, say, copy_file_to_from and using positional arguments instead.

Moreover, you have the freedom to choose the order of the arguments:

copy_file(from: 'upload/bar.txt, to: 'tmp/foo.txt')

Keyword Arguments vs Positional Arguments

Getting back to the previous words example, note that you can’t pass sentence as a positional argument:

words('I like it!')
# => ArgumentError: wrong number of arguments (given 1, expected 0)

You can also mix positional with keyword arguments - if the positional ones come first - and define a default value:

def words2(sentence, separator: ' ')
  sentence.tr(',.!?;', '').split(separator)
end
words2('I like it'
=> ["I", "like", "it"
words2('I+like+it', separator: '+')
=> ["I", "like", "it"]

Default Values and Special Features

Here’s a (contrived) example also involving positional arguments that have a default value:

def words3(sentence,             # mandatory positional argument
           punctuation='.,!?;',  # positional argument with default
           separator: ' ',       # keyword argument with default
           shout:)               # mandatory keyword argument
  input = shout ? sentence.upcase : sentence
  input.tr(punctuation, '').split(separator)
end
words3('ok, enough!', shout: true)
# => ['OK', 'ENOUGH']

When you pass a Hash as the only argument, it will be automatically converted to keyword arguments:

def message(greeting:, first_name:, last_name:)
  "#{greeting} #{first_name} #{last_name}!"
end
args = {greeting: 'Hello', first_name: 'Alan', last_name: 'Turing'}
message(args)
# => "Hello Alan Turing!"

Ruby will check if you pass an unknown parameter:

args = {greeting: 'Hello', first_name: 'Alan', last_name: 'Turing', middle_name: 'Mathison'}
message(args)
# => ArgumentError: unknown keyword: middle_name

Double Splat Operator

By using the double splat operator (**), you can explicitly convert a Hash to keyword argument (analogous to how the splat operator * lets you convert an Array to a positional argument):

def message2(greeting, first_name:, last_name:)
  "#{greeting} #{first_name} #{last_name}!"
end
args = {first_name: 'John', last_name: 'McCarthy'}
message2('Hello', **args)
# => "Hello John McCarthy!"

If the double splat operator is used in the argument list, the argument will contain all unknown keyword arguments as Hash (analogous to how the splat operator works for positional parameters):

def message3(greeting:, first_name:, last_name:, **rest)
  # `rest` is just a regular Hash here
  info = rest.map { |k,v| 
                    "your #{k.to_s.tr('_', ' ')} is #{v}" 
                  }.join(', ')
  "#{greeting} #{first_name} #{last_name}! Information about you: #{info}"
end
message3(greeting: 'Hello', 
         first_name: 'Alan', 
         last_name: 'Turing', 
         middle_name: 'Mathison', 
         birth_date: '1912-06-23')
# => "Hello Alan Turing! Information about you: your middle name is Mathison, 
      your birth date is 1912-06-23"

This concludes our tour of keyword arguments in Ruby. Let’s have a look at how they’re implemented in Python. This is interesting because Python makes some different choices.

Python

Syntax

In addition to some syntax, Python’s main difference to Ruby is that the calling code can, by default, decide whether to set the value of a parameter *in function call using positional or keyword arguments:

# I'm using Python 3.x in these code examples
# In case '=' appears in the function signature, it means "default value"
def message(greeting, first_name, last_name, punctuation_mark='!'):
    return f"{greeting} {first_name} {last_name}{puntuation_mark}"
message('Hello', 'Guido', 'van Rossum')
message(greeting='Hello', first_name='Guido', last_name='van Rossum')
message('Hello', 'Guido', last_name='van Rossum')
message(greeting='Hello', last_name='van Rossum', first_name='Guido')
# Return value for each of these examples:
# => 'Hello Guido van Rossum!'

Keyword Arguments vs Positional Arguments

You must provide positional arguments before keyword arguments, and you can’t set the same parameter more than once. Moreover, if you pass an unknown parameter, there will be an error.

message(greeting='Hello', 'van Rossum', 'Guido')
# => SyntaxError: positional argument follows keyword argument
message('Hello', 'Guido')
# => TypeError: message() missing 1 required positional argument: 'last_name'
message('Hello', greeting='Hi', first_name='Guido', last_name='van Rossum')
# => TypeError: message() got multiple values for argument 'greeting'
message('Hello', first_name='Guido', last_name='Rossum', middle_name='van')
# => TypeError: message() got an unexpected keyword argument 'middle_name'

args and kwargs

To avoid the last error, you can - similarly to Ruby’s double splat operator - provide a final formal parameter of the form **name; it will contain a dictionary containing all keyword arguments except for those corresponding to a formal parameter.

def message2(greeting, first_name, last_name, **kwargs):
    # `kwargs` is just a regular dictionary here (kwargs is used as name by convention)
    info = ', '.join(
                      [f"your {k.replace('_', ' ')} is {v}" 
                       for k,v in kwargs.items()]
                    )
                  
    return f"{greeting} {first_name} {last_name}! Information about you: {info}"
message2('Hello', 'Alan', last_name='Turing', middle_name='Mathison', birth_date='1912-06-23')
# => Hello Alan Turing! Information about you: your middle name is Mathison, your birth date is 1912-06-23'

This may be combined with a formal parameter of the form *name, which will contain a tuple of all positional arguments that were not named before. The convention is to name it *args and it must occur before **kwargs, if present.

def f(arg, *args, **kwargs):
   return [arg, args, kwargs]
f(1, 2, 3, a=5, b=8)
# => [1, (2, 3), {'a': 5, 'b': 8}]
f(arg=1, a=2, b=3, c=5, d=8)
# => [1, (), {'a': 2, 'b': 3, 'c': 5, 'd': 8}]

Mandatory Keyword Arguments

Any parameters declared after the *args formal parameter must be passed as keyword arguments, otherwise Python won’t know when to stop collecting the *args.

def concat(*words, sep=", "):
    return sep.join(words)
concat('Andorra', 'Argentina', 'Azerbaijan')
# =>'Andorra, Argentina, Azerbaijan'
concat('Andorra', 'Argentina', 'Azerbaijan', sep=' --- ')
# => 'Andorra --- Argentina --- Azerbaijan'
concat('Andorra', 'Argentina', 'Azerbaijan', ' === ')
# => 'Andorra, Argentina, Azerbaijan,  === '

This can be used to force the client code to use keyword arguments.

def move(*_, source, target):
	return f"Moving from {source} to {target}"
move(source='Andorra', target='Argentina')
# => 'Moving from Andorra to Argentina'
move('this', 'is', 'ignored', source='Andorra', target='Argentina')
# => 'Moving from Andorra to Argentina'
move('this', 'does', 'not', 'work', 'Andorra', 'Argentina')
# => TypeError: move() missing 2 required keyword-only arguments: 'source' and 'target'
move('Andorra', target='Argentina')
# => TypeError: move() missing 1 required keyword-only argument: 'source'

Interestingly, mandatory keyword arguments are the only way Ruby supports the feature. This is an interesting contrast to one of Python’s guiding principles, namely “There should be one-- and preferably only one --obvious way to do it.” In this case, if you’re a library programmer, for example, you leave more freedom to *the client code than e.g. Ruby.

Lastly, let’s have a look at Clojure, which approaches keyword arguments from a completely different angle.

Clojure

Clojure is an interesting case, because - strictly speaking - there is no special, dedicated syntax in Clojure for keyword arguments. Instead, programmers can, for example, pass a map (the equivalent of a Hash/dict) as argument to functions. (This is what Ruby programmers did prior to the introduction of keyword arguments with Ruby 2.0, too.)

To avoid having to manually extract the values from the map, Clojure offers map destructuring (also called map binding forms). This mini sub-language is not limited to function definitions; it can be used practically everywhere. The syntax is tricky and possibly unfamiliar, but it’s powerful. Let’s introduce it slowly by looking at a basic form where it’s used, the built-in let form:

The let Form

(let [a 1, b 2]    ;; in this example, `a` is a _binding form_, `1` is an _initialization expression
	(str a " " b))
;; => "1 2"

A vector can be used as binding form (the left side) to initialize variables with a sequence of values:

      ; binding form  ; init expression
(let [[a b c]         [1 2 3]]
	(str a " " b " " c))
;; => "1 2 3"

Binding Maps

Analogously, a map can be used as binding form (then called binding map) to initialize a map:

      ; binding map         ; init expression
(let [{a :a_key, b :b_key}  {:a_key 1, :b_key 2}]
   	(str a " " b))
;; => "1 2"

Note that the values of the map in the binding form are keys of the map in the initialization expression.

The most common case is that the variables you’re setting in the binding form have the same name as the keys of the init expression:

      ; binding map    ; init expression
(let [{a :a, b :b}     {:a 1, :b 2}]
   	(str a " " b))
;; => "1 2"

:keys and :or

Therefore, this case has a shortcut, using the special :keys key in the binding map:

      ; binding map   ; init expression
(let [{:keys [a b]}   {:a 1, :b 2}]
   	(str a " " b))
;; => "1 2"

To define default values in case, the init expression doesn’t have a key. There is the special :or key in the binding map where you can provide a map of default values:

      ; binding map                      ; init expression
(let [{:keys [a b], :or {:a 10, b 20}}   {:a 1}]
   	(str a " " b))
;; => "1 20"

Function Definitions

Finally, after this lengthy detour, we can now move on to function definitions! We can use this technique inside a function definition like this:

(defn message [args]
  (let [{:keys [greeting first-name last-name]} args]
    (str greeting " " first-name " " last-name)))
(message {:greeting "Hello" :first-name "Rich" :last-name "Hickey"})
;; => "Hello Rich Hickey"

Or, more succinctly, we use a map binding form directly in the parameters:

(defn message [{:keys [greeting first-name last-name],

<p id="gdcalert1" ><span style="color: red; font-weight: bold">>>>>>  gd2md-html alert: Definition &darr;&darr; outside of definition list. Missing preceding term(s)? </span><br>(<a href="#">Back to top</a>)(<a href="#gdcalert2">Next alert</a>)<br><span style="color: red; font-weight: bold">>>>>> </span></p>
                :or {greeting "Yo"}}]
  (str greeting " " first-name " " last-name))
(message {:first-name "Rich" :last-name "Hickey"})
;; => "Yo Rich Hickey"

So you see, if you just look at the code that calls the function, you see the “name” of the parameter we’re setting. This is basically how Clojure implements keyword arguments.

Original Map with :as

In a map binding form, to bind the original map, the special :as map key can be used:

(defn full-name [{:keys [first-name last-name]}]
  (str first-name " " last-name))
(defn message [{:keys [greeting first-name last-name],

<p id="gdcalert2" ><span style="color: red; font-weight: bold">>>>>>  gd2md-html alert: Definition &darr;&darr; outside of definition list. Missing preceding term(s)? </span><br>(<a href="#">Back to top</a>)(<a href="#gdcalert3">Next alert</a>)<br><span style="color: red; font-weight: bold">>>>>> </span></p>

                :or {first-name "Alan"}

<p id="gdcalert3" ><span style="color: red; font-weight: bold">>>>>>  gd2md-html alert: Definition &darr;&darr; outside of definition list. Missing preceding term(s)? </span><br>(<a href="#">Back to top</a>)(<a href="#gdcalert4">Next alert</a>)<br><span style="color: red; font-weight: bold">>>>>> </span></p>

                :as message-props}]
  (str greeting " " (full-name message-props)))
(message {:greeting "Hello" :last-name "Turing"})
;; => "Hello Alan Turing"

Keyword Arguments Without Passing a Map

There is also a trick to avoid having to pass a map, which uses the & syntax to define a variable number of arguments (analogous to the *args syntax from Ruby and Python we’ve seen above), together with a map binding form:

(defn message [& {:keys [greeting first-name last-name],

<p id="gdcalert4" ><span style="color: red; font-weight: bold">>>>>>  gd2md-html alert: Definition &darr;&darr; outside of definition list. Missing preceding term(s)? </span><br>(<a href="#">Back to top</a>)(<a href="#gdcalert5">Next alert</a>)<br><span style="color: red; font-weight: bold">>>>>> </span></p>

                  :or {greeting "Yo"}}]
  (str greeting " " first-name " " last-name))
(message :first-name "Rich" :last-name "Hickey") ;; notice the missing {}, i.e. we don't pass a map
;; => "Yo Rich Hickey"

What’s going on here? First of all, (:first-name "Rich" :last-name "Hickey) is passed as list to the message function (because of the &). Then, because Clojure sees a map binding form, it converts the list to the map {:first-name "Rich" :last-name "Hickey}, and then continues with binding the values to the variables.

At first, this may look appealing, as it matches more closely what we’ve seen above in Ruby and Python. However, it has some significant drawbacks:

  • Let’s assume both full-name and message used this technique from the example above. It turns out the default values are not taken into account for message-props in this case (i.e. the output would be just “Hello Turing” because the first name is nil within full-name).

  • Moreover, you can’t just call full-name with message-props. You would first need to “flatten” the map to a list of arguments. This can be solved by using the mapply form. Then again, mapply isn’t even part of the standard library.

  • Using a separate map gives you the opportunity to treat the message-props as first-class citizens, e.g. creating a separate constructor method, where you can validate input. In particular, you could use clojure.spec to verify that certain keys are set (similar to Ruby’s mandatory keyword arguments).

Therefore, I suggest you really just pass a map to emulate keyword arguments in Clojure.

Conclusion

Let’s look at the Pros and Cons of keyword arguments:

Pros:

  • Makes code more readable, because - by definition - each argument is named.

  • Flexibility. The arguments can be passed in any order. Default values can easily be provided for each argument.

  • The last point is more subtle, but importantly: keyword arguments encourage adding new parameters instead of changing existing ones because the corresponding client code doesn’t need to be changed. That is, it makes it easier for e.g. library programmers to not introduce breaking changes, making refactoring easier and the code more reliable for other code that depends on it. Adding a new parameter is easy and will not break existing code. When renaming a parameter, you could still accept the old name in order to avoid breaking existing code.

Drawbacks:

  • It’s more verbose, less concise. For very basic functions, I wouldn’t recommend it: add(summand1: 1, summand2: 2) is just ugly.

  • It encourages designing functions with too many arguments - because it’s so easy to add more. You should be aware of this and try to decompose large functions to simpler parts.

In general, I believe keyword arguments are often a fantastic choice, assuming you’re using a language that supports them. Interestingly, many modern languages choose not to support them, and there are surely good reasons not to support them - that’s the topic of another blog bost 😃. However, if they are offered, I believe quite often programmers coming from more traditional languages such as C, C++ or Java tend to underuse them out of habit. Mind you, I’m not saying they should always be used - there is always a trade-off. But possibly, you should consider using them more often!

With this, I’m closing this hopefully fun tour, and I’ll be happy to hear any thoughts in the comments.