Update Feb. 10, 2010: I was wrong. Recent discussions indicate that placing optional arguments in-line, as in my first example, is preferred. In the future, Clojure may have destructuring support for this style. For now, this post remains a useful guide to map destructuring.
Many languages, such as Python and Ruby, allow functions arguments to be passed as name-value pairs.
People often ask for the same thing in Clojure, and they end up writing something like this:
(defn foo [a b & options] (let [opts (apply hash-map options)] ...)) (foo 1 2 :optionA 3 :optionB 4)
That works, but it’s not very efficient. Every time you call foo, it has to construct a hash-map from its arguments. That’s a fair amount of overhead for what is usually a small map of options.
A better way is to use Clojure’s map binding forms. The syntax is tricky, but very powerful.
Let’s look at the general syntax of Clojure’s local binding form, let:
(let [bindings...] expressions...)
The bindings are pairs consisting of a binding form and an initialization expression. The simplest binding pair is just name-value:
(let [a 1, b 2] (list a b)) ;;=> (1 2)
So far, so good. You probably know that you can use a vector as the binding form to bind sequential things like lists, vectors, and even strings:
(let [[a b c] "foo"] (list a b c)) ;;=> (\f \o \o)
Moving on: A map binding uses a map as the binding form. Well, duh. Let’s call it the binding map. (catchy, no?)
The keys of the binding map are the local variables you want to create. The values of the binding map are keys in the initialization expression. The locals will be bound to the values of corresponding keys.
How about an example?
; binding form ; init expression (let [{a :keyA, b :keyB} {:keyA 1, :keyB 2}] (list a b)) ;;=> (1 2)
You can use the same technique for function arguments:
(defn foo [{a :keyA, b :keyB}] (list a b)) (foo {:keyA 1 :keyB 2}) ;;=> (1 2)
Notice that we’re calling foo with a single argument, a map. This may be slightly less pretty than having key/value pairs directly in the function arguments, but it is more efficient, since the compiler knows exactly how big the argument map is. It’s also more flexible, because we can construct the argument map elsewhere and then pass it to the function:
(def options {:keyA 3, :keyB 4}) (foo options) ;;=> (3 4)
You can also mix normal, positional arguments with map arguments:
(defn foo [a b {c :keyC, d :keyD}] (list a b c d)) (foo 1 2 {:keyC 3, :keyD 4}) ;;=> (1 2 3 4)
In the most common case, you probably want your local variables to have the same names as the keywords in the initialization expression. Clojure has a shortcut for that, a special key in the binding map called, appropriately enough, :keys:
(defn foo [{:keys [a b]}] (list a b)) (foo {:a 5, :b 6}) ;;=> (5 6)
You can also supply a map of default values using another special key in the binding map, the :or key:
(defn foo [{:keys [a b c], :or {c 42}}] (list a b c)) (foo {:a 7, :b 8, :c 9}) ;;=> (7 8 9) (foo {:a 20, :b 30}) ;;=> (20 30 42)
Finally, what if you want to enforce a requirement that certain keys be present? Clojure 1.1 introduced a great way to do that with pre- and postconditions. You can set a pre-condition that will throw an exception if a particular binding is nil:
(defn foo [{:keys [a b c]}] {:pre [(not (nil? c))]} (list a b c)) (foo {:a 1, :c 3}) ;;=> (1 nil 3) (foo {:b 2}) ;;=> java.lang.AssertionError: Assert failed: (not (nil? c))
There are even more features of map binding that I haven’t covered here. But this should give you enough background to understand the full documentation.
Should probably be pointed that this mixes really well with normal parameters:
(defn text-input [name value & [{:keys [size class]}] ]
[:input {:name name :value value :size size :class class}])
and can be called either:
(text-input “first_name” “John”)
or
(text-input “first_name” “John” {:size 20})
Yes, it mixes well. Although I would have written your example as a multiple-arity function, like this:
(defn text-input
([name value] (text-input name value {}))
([name value {:keys [size class]}] …))
sweet. Didn’t know that (let [ { :keys [a b] } {:a 1 :b 2} ] [a b] ) was a special case of (let [{a :a b :b} {:a 1 b: 2}] [a b] ). Thanks!
Now we have to construct a map on the caller site. But it will probably be an array-map which is faster to construct.
And we can’t use apply anymore:
(let [arguments [x y :key1 val1 :key2 val2]] (apply my-fn a b arguments))
Raises the question whether one needs apply with keyword arguments.
Getting away from the example:
Nice coverage of map destructuring. :)
[…] Keyword Arguments in Clojure, the Right Way […]