A Brief Intro to Destructuring in Clojure

2015-01-03

While going through the Clojure Koans a second time, I was surprised by how simple destructuring is. To make the point clear, here is a short introduction to the DSL.

First, we will start with a basic example. Assuming we have an array of two elements, we can easily associate each element with its own binding.

(defn f [[a b]]
  {:a a
   :b b})

(f ["foo" "bar"]) ;; => {:a "foo", :b "bar"}

By declaring the arguments to the function f as a nested vector, we are telling Clojure to expect a two element vector as an argument, and to bind the first element to a and the second element to b within the scope of the function. By then invoking f with ["foo" "bar"], we see that the result does bind as we expect. Note that if we pass anything less than a two element vector, the respective bindings will simply be nil.

(f ["foo"]) ;; => {:a "foo" :b nil}
(f [])      ;; => {:a nil   :b nil}

In the case a multi-element vector, we can destructure as many individual elements as we wish all while capturing the rest in a seq.

(defn f [[a b & more]]
  {:a a
   :b b
   :more more})

(f ["foo" "bar" "baz" "quux"])
;; => {:a "foo", :b "bar", :more ("baz" "quux")}

As we can see from the output, :a and :b are just like the example above, but this time the remaining elements of the vector have been collected into a seq. The choice of more is entirely arbitrary; any name would be perfectly fine.

An alternate form similar to & more above is the use of the :as keyword which creates a binding to the original argument, in addition to whatever destructuring we require.

(defn f [[a b :as original-arg]]
  {:a a
   :b b
   :original-arg original-arg})

(f ["foo" "bar"])
;; => {:a "foo", :b "bar", :original-arg ["foo" "bar"]}

So far we have covered destructuring of vectors. Lists work much the same. When we reach maps, though, we have a slightly different approach.

Given a map of key value pairs, we can create bindings for the values based on their keys.

(defn f [{a :foo b :bar}]
  {:a a
   :b b})

(f {:foo "my foo" :bar "my bar"})
;; => {:a "my foo", :b "my bar"}

By providing a map within the vector of arguments to the function f, we tell Clojure to expect a map as an argument, to look up the keys :foo and :bar in that map, and to bind the associated values of those keys to a and b respectively. For clarity, I have chosen a local binding which is distinct from the corresponding map key. In common use, though, we will often want to use a local binding which matches the key it is based upon. For example, rather than binding :foo to a, we might have instead bound :foo to foo.

Because binding a certain key to a symbol of the same name (e.g., :foo to foo) is such a common operation, Clojure provides a shorthand syntax.

(defn f [{:keys [foo bar]}]
  {:a foo
   :b bar})

(f {:foo "my foo", :bar "my bar"})
;; => {:a "my foo", :b "my bar"}

By using the :keys keyword with a vector of key names, Clojure creates local bindings whose names match the key.

Note that the :as keyword works just as we saw above with vectors:

(defn f [{:keys [foo bar] :as original-arg}]
  {:a foo
   :b bar
   :original-arg original-arg})

(f {:foo "my foo", :bar "my bar"})
;; => {:a "my foo", :b "my bar" :original-arg {:foo "my foo", :bar "my bar"}}

There are likely more details of destructuring that I have missed here. Nonetheless, the above examples represent the basics of destructuring.