Keyword Arguments: Ruby, Clojure, Common Lisp

And suddenly I return to blogging, rising from the ashes like some kind of zombie phoenix. Turns out writing a book is a good absorber of time, like some sort of heavy-duty temporal paper towel. Now that I've gotten the terrible similes out of my system, let's talk about keyword arguments, one of my favorite features in any language that supports them.

Ruby, Clojure, and Common Lisp are all languages I enjoy to some degree, and they all have keyword arguments. Let's explore how keyword args differ in those languages.

Why keyword arguments?

Why are keyword arguments good?

  1. You can omit arguments.
  2. You can supply arguments in an arbitrary order.
  3. Arguments are labeled, so you know what argument means what.

Positional arguments require mentally lining up the 7th argument in your function call with the 7th argument in the function signature, and so on. Keyword arguments become more and more attractive the more arguments you have in your function signature.

Keyword arguments trade a bit of verbosity for added explicitness, clarity and reduced mental burden. (Kind of like Lisps do overall. Fancy that.)

Ruby

Ruby doesn't have special support for keyword arguments. But Ruby likes its hashes, so you can just pass one in as an argument to a function. As some syntax sugar, if you pass a "flat" list of :key => val pairs, Ruby slurps them all together and stuffs them into a hash for you.

def foo(arg)
  p arg
end

foo({:x => 123})                  # => {:x=>123}
foo(:x => 123)                    # => {:x=>123}

With even more added sugar, you can leave off the parens in Ruby function calls. So this is pretty common in Ruby:

foo :x => 123                     # => {:x=>123}

How nice and punctuation-less. But then things get ugly. What about this?

foo {:x => 123}

That won't even compile. Ruby thinks {:x => 123} is a code block, and a bare key/value pair isn't valid syntax as the first thing in a code block. You need the parens. A bit unfortunate, but it gets worse...

def bar(arg1,arg2)
  puts "#{arg1} #{arg2}"
end

bar :x => 123, :y => 456          # Runtime error
bar {:x => 123}, {:y => 456}      # Won't compile

In the first example, all of the key/value pairs are slurped into one hash and end up in arg1. There's nothing left for arg2, so you get a "wrong number of arguments" exception. The second example won't even compile, of course, because again Ruby thinks {:x => 123} must be a code block with invalid syntax.

It gets worse if you change the argument list for bar slightly...

def bar2(arg1 = {}, arg2 = {})
  puts "#{arg1} #{arg2}"
end

This way, you don't even have to supply any arguments. This is nice, if all of your argument are optional.

bar2    # => {} {}

Now suppose you want arg1 to be {:x => 123}, and arg2 to be {:y => 456}. You might naively try this:

bar2 :x => 123, :y => 456         # => {:x=>123, :y=>456} {}

Oops, it all got dumped into arg1, and now instead of a missing argument error, arg2 silently ends up with a default, empty hash. You have to explicitly pass in an empty value for arg1 so that everything is slurped into arg2.

bar2 {}, :x => 123, :y => 456     # => WRONG!  Ruby thinks {} is a code block again.

bar2({}, :x => 123, :y => 456)    # => {} {:x=>123, :y=>456}

So much for syntax sugar. You might think you'd be unlikely to find this kind of thing in the wild, but in Ruby on Rails for example, there are quite a few functions whose argument lists look exactly like this. One signature for link_to is:

link_to(body, url_options = {}, html_options = {})

So...

link_to "foo", :controller => :x                          # OK
link_to "foo", :controller => :x, :class => "css_class"   # WRONG! 
link_to "foo", {:controller => :x}, :class => "css_class" # OK

What about support for default arguments? We might want to say that if you didn't pass in an :x argument, we want it to have some default value. You might think this would work:

def baz(x = {:x => 123})
   p x
end

But you would be sadly mistaken.

baz                               # => {:x=>123}
baz :y => 456                     # => {:y=>456} ... oops

Ruby doesn't merge your keyword arguments into the map in the parameter list. It uses that map if you don't supply any arguments, otherwise your map replaces the default entirely. So to get default arguments, you need something like

def baz2(args = {})
  args = {:x => 123}.merge(args)
  p args
end

baz2                              # => {:x=>123}
baz2 :x => 555                    # => {:x=>555}
baz2 :y => 456                    # => {:x=>123, :y=>456}

Kind of messy, but that's OK.

One last subtle ambiguity in Ruby is determining whether someone passed a nil argument for a keyword explicitly, or omitted a keyword entirely. It might make a difference in some circumstances.

def quux(args={})
  p args.include? :x
  args = {:x => nil}.merge(args)
  p args
end

quux                              # true, {:x=>nil}
quux :x => nil                    # false, {:x=>nil}

Pretty messy, but such is life.

Clojure

Part of the fun of Lisps is lack of ambiguity. Everything is spelled out in all its parenthesized glory. In Clojure, when you call a function like (foo :x 123), Clojure requires you to specify what you want to happen with those arguments.

It used to be that Clojure didn't have much support for keyword args at all. Clojure did have support for allowing variable numbers of arguments to functions though. So in the beginning, people used to slurp all of their arguments together into a list, and then turn it into a map inside the function.

user> (defn foo [& args]
        (let [args (apply hash-map args)]
          (prn args)))
#'user/foo
user> (foo)
{}
user> (foo :x 123)
{:x 123}
user> (foo :x 123 :y 456)
{:y 456, :x 123}

That worked. It still works today. But nowadays there's a better way. Why not slurp your arguments directly into a hash-map?

user> (defn foo [& {:as args}]
        (prn args))
#'user/foo
user> (foo)
nil
user> (foo :x 123)
{:x 123}
user> (foo :x 123 :y 456)
{:y 456, :x 123}

This is an example of destructuring. In this case, all of our arguments are slurped into a list (thanks to &), then this list is matched against our destructuring pattern, in this case {:as args}. :as says to take everything in the list, turn it into a map and give the resulting map the name args.

This isn't a special feature of defn. Destructuring works anywhere you're setting up bindings, for example in let, for, doseq etc. Like so:

user> (let [{:as args} (list :x 123 :y 456)] args)
{:y 456, :x 123}

It just so happens that & creates the list for you, out of the arguments you pass.

Destructuring does lots more than that though. We can immediately pull out the values for keywords we care about, so they'll be bound to names in our function body.

user> (defn foo [& {:keys [x y z]}]
        (prn x y z))
#'user/foo

user> (foo :z 123 :x 456)
456 nil 123

It's certainly a bit more verbose than Ruby in the function signature, but it lacks ambiguity and it saves you some repetition in the function body.

Clojure's approach also has the benefit of specifying directly in the function signature which keywords you're expecting. In a smart editor, like Emacs, you get an indicator of what kinds of keywords you should be passing in. See at the bottom?

Emacs function signature display

This is also available at the Clojure REPL via the doc function.

user> (doc foo)
-------------------------
user/foo
([& {:keys [a b c]}])
  nil
nil

There's no better documentation than live, built-in documentation. There's nothing more distracting when programming than context shifts, and having to dig into a web browser to check a function signature is a huge mental page fault.

What about default values? Sure. You can use :or to specify defaults for some or all of your keywords. This works much more like I'd expect, compared to Ruby.

user> (defn foo [& {:keys [x y z] :or {x 1 y 2 z 3}}]
        (prn x y z))
#'user/foo

user> (foo :y 555)
1 555 3

What about determining whether the user passed nil for a keyword or whether they omitted the keyword entirely? For that, you have to resort to testing the argument map for the existence of the key, which isn't fun, but at least it's possible.

user> (defn foo [& {:keys [x y z] 
                    :or {x 1 y 2 z 3}
                    :as args}]
        (prn x y z)
        (prn args)
        (doseq [k [:x :y :z]]
          (println "Contains" k "=>" (contains? args k))))
#'user/foo

user> (foo :x 1 :y 2)
1 2 3
{:x 1, :y 2}
Contains :x => true
Contains :y => true
Contains :z => false

Why do I keep mentioning this? See Common Lisp below.

In any case, our parameter list is becoming huge and unweildy. The first time I saw sample Clojure code like this, I almost did a spit-take. But after a bit of getting used to, I'm finding that this syntax is pretty comfortable.

The simplest case may not be as concise as Ruby, but aside from lack of ambiguity, the benefit of Clojure's approach is being able to safely do powerful (and borderline insane) things, like nested destructuring. And this works everywhere you're setting up a binding.

(defn ow-my-eyes [a & {[b {:keys [x y]}
                        & {[g h] :r
                           :keys [p q]
                           :or {q 123}}]
                       :x}]
  (prn a b g h p q x y))



user> (ow-my-eyes 444 :x [1 {:x 555 :y 666} :p 3 :r [888 999]])
444 1 888 999 3 123 555 666

If you wrote code like that in real life, you'd likely be defenestrated, but at least you know you can do it. And destructuring in Clojure could likely be extended even further in the future, if someone came up with a use case for something that isn't supported.

Official documentation for all of this is here.

Common Lisp

When it comes to keyword arguments, Common Lisp supports mostly everything that Clojure does, and some things it doesn't. CL was likely a big inspiration for Clojure's destructuring. I highly recommend reading Practical Common Lisp to learn more about CL keyword arguments and list destructuring. (Read the rest of the book while you're at it.)

Keyword arguments in CL look like this:

> (defun foo (&key x (y 123) (z 456 z-supplied-p))
    (pprint (list x y z z-supplied-p)))
FOO

> (foo)
(NIL 123 456 NIL)

> (foo :z nil)             ; note, z-supplied-p tells us whether z was omitted or not
(NIL 123 NIL T)

> (foo :z 555 :x 666 :y 777)
(666 777 555 T)

> (foo :x 123)
(123 123 456 NIL)

> (foo :x 555)
(555 123 456 NIL)

Yeah, there's direct support for distinguishing supplied keys with nil values, and un-supplied keys. That's pretty nice.

Common Lisp also supports insane things like having keyword arguments' default values be a function of other arguments in the parameter list.

> (defun bar (&key (x 123) (y (+ x 1000)))
    (pprint (list x y)))
BAR

> (bar)
(123 1123)

> (bar :x 5)
(5 1005)

> (bar :x 5 :y 6)
(5 6)

Clojure destructuring can't do this by default, so you'd have to resort to a let in the function body.

(defn bar [& {:keys [x y] :or {x 123}}]
  (let [y (or y (+ x 1000))]
    (prn x y)

There's nothing stopping Clojure's destructuring from being patched to support this, of course. But this isn't a feature I've ever found myself wanting particularly badly.

As for how to destructure CL keyword arguments into a vector or hash-map, CL doesn't directly support doing it in function parameter lists like Clojure does. (Though there's nothing stopping someone from throwing together a reader macro to let CL do this, of course.) This isn't really surprising, because CL loves its cons cells, while Clojure embraces maps (and vectors and sets etc.), offering them default reader syntax and lots of other built-in support.

So there you have it. Keyword arguments. Use them. Love them.

Lessons:

  1. Sugar can be bad for your health.
  2. Ambiguous, bad. Explicit, good.
June 24, 2011 @ 10:22 AM PDT
Cateogory: Programming
Tags: Lisp, Ruby, Clojure

12 Comments

Brett
Quoth Brett on June 24, 2011 @ 3:46 PM PDT

If you have "seven arguments" to a function, you're almost certainly doing it wrong. Functions should be simple and cohesive. Lots of arguments means you're probably trying to do way too much in one place.

That said, keyword arguments are nice... but I fear they are sometimes used to hide the symptoms caused by poorly organized code.

Dan Ballard
Quoth Dan Ballard on June 25, 2011 @ 12:30 AM PDT

Yeah! Glad to see you back!

angel
Quoth angel on June 25, 2011 @ 4:20 AM PDT

I don't know if is a good idea comparing and criticize to ruby, ruby is a lovely language and if it has something than I can love...It's precisely its ambiguity and its different ways for do the same thing, I get bored when I use python where only I has one way do it...now...

I like clojure a lot too...and destructuring is a special feature than when you see it first time look complicate and without importance, but it's a mistake..destructuring is a powerfull tool and it help do readable code, simple, powerfull, I've short time using clojure and I must say it's something than I miss in other languages...

I think a bit different to brett because clojure is a language without OOP and normally is better and readable destructuring your "object" (a hashmap or structmap) and take the important thing than take the whole object...

thanks and sorry for my bad english...I'm italian

Michael Edgar
Quoth Michael Edgar on June 25, 2011 @ 9:11 AM PDT

Ruby added a new syntax for symbol-as-hash-key in Ruby 1.9, primarily to improve this approach:

foo(x, y, :bar => baz, :quux => zap)

becomes

foo(x, y, bar: baz, quux: zap)

Not everybody is a fan. I happened to actually publish a gem today that translates between the two syntaxes.

Brian
Quoth Brian on June 25, 2011 @ 3:53 PM PDT

Michael: Yeah, not sure I see the point of the new syntax. It seems to be fixing a problem that doesn't exist. Unless I'm missing something. Saving a few characters and making Ruby look like Javascript seems like a silly reason to dork around with the language on that level.

angel: I do love Ruby still, it has strengths and weaknesses. I still use Ruby for a lot of things.

Brian
Quoth Brian on June 25, 2011 @ 4:00 PM PDT

Brett: Imagine a function to start a simulation with a lot of parameters, where all parameters have defaults but some you might want to override (number of trials, number of iterations per trial, value of X, value of Y etc.). I've run into that before.

And now I'm getting horrible flashbacks of certain Windows APIs where you had to supply a half dozen nulls to reach the parameter you wanted to specify. I can't defend such a thing as a good API.

I'm not in the habit of writing functions with 7 parameters, no.

Nick Coghlan
Quoth Nick Coghlan on June 26, 2011 @ 5:23 PM PDT

Anyone dealing with hardware interfaces and anything else with tons of tunable parameters typically loves keyword arguments :)

For example:

async = AsyncSerial(data_rate=2400, parity="odd") sync = SyncSerial(data_rate=9600, sync_pattern=0x90EB)

I don't need to care about the order of the underlying arguments in the constructor, I just need to know their names and their default values.

Lucian
Quoth Lucian on June 28, 2011 @ 11:55 AM PDT

I quite like how Python does this:

def f(a, b=2): print(a, b)

f(1, 2) f(b=1, a=1) f(2)

Compact and unambiguous.

Slurping is also possible

def f(a, b=2, args, *kwargs): ...

f(4, 5, 1, 2, 3, bla=2) # a = 4, b = 5, args = [1, 2, 3], kwargs = {bla:2}

mc
Quoth mc on June 28, 2011 @ 3:40 PM PDT

Thanks for the interesting language comparison.

Here's another small but useful example of using named args in Clojure to create a more flexible "range" function:

(defn narg-range "Named argument version of range, :b begin :e end :s step. Defaults are :b 0 :e infinity :s 1. All parameters are optional." [& {:keys [b e s]}] (range (if b b 0) (if e e Double/POSITIVE_INFINITY) (if s s 1))) http://mchampine.wordpress.com/2011/02/15/named-argument-version-of-range/

Sam Aaron
Quoth Sam Aaron on June 28, 2011 @ 6:49 PM PDT

Really interesting post - thanks. I've always been annoyed by explicit nil vals being clobbered by map merges - it's nice to see that CL has explicit support for this.

BTW, quick pointer - in your last Ruby example you have the comment outputs the wrong way round. It should be:

quux                              # false, {:x=>nil}
quux :x => nil                    # true, {:x=>nil}
Quoth on July 05, 2011 @ 7:00 PM PDT

Keyword arguments? Aren't those actually called named arguments, or even named parameters? Keywords are things like 'if', 'for' or 'return'.

roger
Quoth roger on January 17, 2012 @ 2:07 AM PST

So with the clojure example here I assume it disallows people passing in keys that aren't in the list?

Speak your Mind

You can use Markdown in your comment.
Email/URL are optional. Email is only used for Gravatar.

Preview