My Profile Photo

Bits and pieces


Between bits and bytes and all other pieces.
A tech blog about Clojure, Software Architecture, and Distributed Systems


The complete guide to Clojure destructuring.

Updated to Clojure 1.10.1 - Last update on 2019-07-23.

Destructuring is a simple, yet powerful feature of Clojure. There are several ways in which you can leverage destructuring to make your code cleaner, with less repetitions, and less bugs.

In this post I want to try to cover all possible ways you can destructure Clojure’s data structures. If I missed anything please, drop me a message and I will include your feedback.

TL;DR Destructuring cheatsheet

Jump at the bottom of the page if you just want to see a destructuring cheatsheet.

What destructuring is?

The simplest form of destructuring is the positional mapping of values from a vector or a list.

Let’s assume that we have a function which somehow returns the current position in terms of latitude and longitude coordinates in the following format: [ lat lng ].

(defn current-position []
  [51.503331, -0.119500])

Now assume we have another function which, accepts two parameters and returns the geohash for a specific location.

(defn geohash [lat lng]
   (println "geohash:" lat lng)
   ;; this function take two separate values as params.
   ;; and it return a geohash for that position
)

Now if we wanted to get the geohash for our current position we would typically do as follow:

(let [coord (current-position)
      lat   (first coord)
      lng   (second coord)]
  (geohash lat lng))

;; geohash: 51.503331 -0.1195

Although this works, there is a lot of boilerplate code for just calling two functions. Destructuring allows you to separate a structured value into its costituent parts. For example the following code is equivalent to the previous one.

(let [[lat lng] (current-position)]
  (geohash lat lng))

;; geohash: 51.503331 -0.1195

Basically the returned value from (current-position) which is a structured value (vector), is getting de-structured into the mapping vector [lat lng] where the first value is assigned to the first element in the vector, the second to the second, and so on.

[lat lng] (current-position)
    |             |
    V             V
[lat lng] [51.503331, -0.119500]


[51.503331, -0.119500]
     |          |
     V          V
   [lat        lng]

The destructuring can be used in any of the binding forms (let, loop, binding, etc) as well as funcation’s parameters and return values.

Now that we understand what destructuring is, we can explore in which ways we can use it to simplify our code.

Destructuring of lists, vectors and sequences.

All sequential data structures in Clojure be destructured in the same way.

(let [[one two three] [1 2 3]]
  (println "one:" one)
  (println "two:" two)
  (println "three:" three))

(let [[one two three] '(1 2 3)]
  (println "one:" one)
  (println "two:" two)
  (println "three:" three))

(let [[one two three] (range 1 4)]
  (println "one:" one)
  (println "two:" two)
  (println "three:" three))

;; one: 1
;; two: 2
;; three: 3

All above s-expr print out the same result.

The example that follow applies in the same way to lists, vectors and sequences. If you are not interested in all values you can capture only the ones you are interested in and ignore the others by putting an underscore (_) as a placeholder for its value.

(let [[_ _ three] (range 1 10)]
  three)
;;=> 3

If you wish capture all the remaining values as a sequence, for example the numbers from 4 to 9, you need to add an ampersand (&) and a symbol to bind to.

(let [[_ _ three & numbers] (range 1 10)]
  numbers)
;;=> (4 5 6 7 8 9)

This is a good way to replace first and rest which often appear in loop/recur construct.

In some cases it is usefull or necessary to keep the full structured parameter as it was originally. Clojure in this case provides the clause :as followed by a symbol.

(let [[_ _ three & numbers :as all-numbers] (range 1 10)]
  all-numbers)
;;=> (1 2 3 4 5 6 7 8 9)

Maps destructuring.

No need to say, maps in Clojure are everywhere. If you pass a map as a parameter to a function, or you return a map out of a function and you want to extract some of the map’s field, then destructuring is what you are looking for.

Let’s go back to our geohashing of the current-position example (first exmaple), if the function current-position rather than returning a vector of two elements it returned a map, this is probably how you would write the code without destructuring.

(defn current-position []
  {:lat 51.503331, :lng -0.119500})

(let [coord (current-position)
      lat   (:lat coord)
      lng   (:lng coord)]
  (geohash lat lng))

;; geohash: 51.503331 -0.1195

Again, nothing wrong with this code, but there is a lot of boilerplate stuff. Think what would happen if you have to extract ten properties. Maps have two ways to destructure data.

(let [{lat :lat, lng :lng} (current-position)]
  (geohash lat lng))

;; geohash: 51.503331 -0.1195

In this case you are telling to the destructuring process to do the following things:

  • Of the returned map, put the content of the key :lat into a local var called lat
  • Of the returned map, put the content of the key :lng into a local var called lng

But again, there is a lot of repetition. This form is useful in some context which will explore later, however most commonly Clojure’s map are destructured in the follwoing way.

(let [{:keys [lat lng]} (current-position)]
  (geohash lat lng))

;; geohash: 51.503331 -0.1195

This form of destructuring is very common as it easily allows you to select which keys are you interested in, however both map Destructuring methods allows you to retain the entire map with the :as clause in the same way of the lists.

(let [{lat :lat, lng :lng :as coord} (current-position)]
  (println "calculating geohash for coordinates: " coord)
  (geohash lat lng))

;; calculating geohash for coordinates:  {:lat 51.503331, :lng -0.1195}
;; geohash: 51.503331 -0.1195


(let [{:keys [lat lng] :as coord} (current-position)]
  (println "calculating geohash for coordinates: " coord)
  (geohash lat lng))

;; calculating geohash for coordinates:  {:lat 51.503331, :lng -0.1195}
;; geohash: 51.503331 -0.1195

From Clojure 1.6 you can also specify the keys as keywords and they can even be namespaced. The following code snippet is equivalent to the previous

(let [{:keys [:lat :lng] :as coord} (current-position)]
  (println "calculating geohash for coordinates: " coord)
  (geohash lat lng))

;; calculating geohash for coordinates:  {:lat 51.503331, :lng -0.1195}
;; geohash: 51.503331 -0.1195

If your keys in your map are not Clojure’s keywords, but strings then you can use the :strs instead of :keys.

(let [{:strs [lat lng] :as coord} {"lat" 51.503331, "lng" -0.119500}]
  (println "calculating geohash for coordinates: " coord)
  (geohash lat lng))

;; calculating geohash for coordinates:  {lat 51.503331, lng -0.1195}
;; geohash: 51.503331 -0.1195

Another option, but not very much used is to have Clojure symbols as keys of your map. In this case you should use :syms instead of :keys

(let [{:syms [lat lng] :as coord} {'lat 51.503331, 'lng -0.119500}]
  (println "calculating geohash for coordinates: " coord)
  (geohash lat lng))

;; calculating geohash for coordinates:  {lat 51.503331, lng -0.1195}
;; geohash: 51.503331 -0.1195

Destructuring maps with default values

Map destructuring with default values is a very powerful feature. Assume that you have function connect-db which takes as input a map with your db configuration. Typically there are a lot of parameters involved, assume that you want to be able to provide default for all or most of the values. One way would be to put the default values in a map and then merge the parameters with the default values. The destructuring offers an easier way to do this.

(defn connect-db [{:keys [host port db-name username password]
                   :or   {host     "localhost"
                          port     12345
                          db-name  "my-db"
                          username "db-user"
                          password "secret"}
                   :as cfg}]
   (println "connecting to:" host "port:" port "db-name:" db-name
            "username:" username "password:" password))

(connect-db {:host "server"})
;; connecting to: server port: 12345 db-name: my-db username: db-user password: secret

(connect-db {:host "server" :username "user2" :password "Passowrd1"})
;; connecting to: server port: 12345 db-name: my-db username: user2 password: Passowrd1

Notice how the default values are injected in the destructured vars only for the keys which are not provided.

Lastly, you can combine the map destructuring with default values with variadic parameters to have functions with default parameters.

If in our example we want to force the user of the connect-db function to provide at least the host where to connect to, but everything else is optional we can write the function as follow:

(defn connect-db [host ; mandatory parameter
                  & {:keys [port db-name username password]
                     :or   {port     12345
                            db-name  "my-db"
                            username "db-user"
                            password "secret"}}]
   (println "connecting to:" host "port:" port "db-name:" db-name
            "username:" username "password:" password))

(connect-db "server")
;; connecting to: server port: 12345 db-name: my-db username: db-user password: secret

(connect-db "server" :username "user2" :password "Passowrd1")
;; connecting to: server port: 12345 db-name: my-db username: user2 password: Passowrd1

Maps destructuring with custom key names

Sometimes it is useful or necessary to destructure maps with a local variable name which name is different than the key name.

(def contact
  {:firstname "John"
   :lastname  "Smith"
   :age       25
   :phone     "+44.123.456.789"
   :emails    "jsmith@company.com"})

(let [{firstname :firstname age :age} contact]
  (str "Hi, I'm " firstname " and I'm " age " years old."))
;;=> "Hi, I'm John and I'm 25 years old."

(let [{name :firstname years-old :age} contact]
  (str "Hi, I'm " name " and I'm " years-old " years old."))
;;=> "Hi, I'm John and I'm 25 years old."

As shown in this second example you can define local var with a name that is different than the key. For example if we have to calculate the distance between two points in a Cartesian plane we can build a function as follow:

(defn distance [{x1 :x y1 :y} {x2 :x y2 :y}]
  (let [square (fn [n] (*' n n))]
    (Math/sqrt
     (+ (square (- x1 x2))
        (square (- y1 y2))))))

(distance {:x 3, :y 2} {:x 9, :y 7})
;;=> 7.810249675906654

Destructuring maps as key-value pairs

Even if maps aren’t stricly speaking sequences, you can easily build a sequence out of a map. Once the sequence is built, the items are key-value pairs which can be destructured as any other vector or list. This is very common when mapping over a map.

(def contact
  {:firstname "John"
   :lastname  "Smith"
   :age       25
   :phone     "+44.123.456.789"
   :emails    "jsmith@company.com"})

(map (fn [[k v]] (str k " -> " v)) contact)
;;(":age -> 25"
;; ":lastname -> Smith"
;; ":phone -> +44.123.456.789"
;; ":firstname -> John"
;; ":emails -> jsmith@company.com")

Nested destructuring

It also possible to do nested destructing. Here is how you can capture content from a nested vector or list.

;; source: wikipedia
(def inventor-of-the-day
  ["John" "McCarthy"
   "1927-09-04"
   "LISP"
   ["Turing Award (1971)"
    "Computer Pioneer Award (1985)"
    "Kyoto Prize (1988)"
    "National Medal of Science (1990)"
    "Benjamin Franklin Medal (2003)"]])

(let [[firstname lastname _ _ [first-award & other-awards]] inventor-of-the-day]
  (str firstname ", " lastname "'s first notable award was: " first-award))
;;=> "John, McCarthy's first notable award was: Turing Award (1971)"

Destructuring nested maps seems a bit more complicated:

(def contact
  {:firstname "John"
   :lastname  "Smith"
   :age       25
   :contacts {:phone "+44.123.456.789"
             :emails {:work "jsmith@company.com"
                      :personal "jsmith@some-email.com"}}})

;; Just the top level
(let [{lastname :lastname} contact]
  (println lastname ))
;; Smith

;; One nested level
(let [{lastname :lastname
       {phone :phone} :contacts} contact]
  (println lastname phone))
;; Smith +44.123.456.789

(let [{:keys [firstname lastname]
       {:keys [phone] } :contacts} contact]
  (println firstname lastname phone ))
;; John Smith +44.123.456.789

(let [{:keys [firstname lastname]
       {:keys [phone]
        {:keys [work personal]} :emails } :contacts} contact]
  (println firstname lastname phone work personal))
;;John Smith +44.123.456.789 jsmith@company.com jsmith@some-email.com

Less common destructuring forms

There are some other forms of destructuring which can be used in particular situations, they are not very common, however it is worth to mention them.

Destructuring vectors by keys

Clojure’s vectors and maps share a lot of commonalities. They are both implemented via Hash array mapped trie (HAMT) (you can find more about their implementation on Ref.1, Ref.2), both support the retrieval by key (or index for vectors) via the get function.

For this reason you can use map’s destructuring methods to destructure vectors. This method might be useful when you have to extract only few keys in high indices. For example assume that you have a vector with 500 elements and you want to extract only the elements at index 100 and 200.

(let [{one 1 two 2} [0 1 2]]
  (println one two))
;; 1 2

(let [{v1 100 v2 200} (apply vector (range 500))]
  (println v1 v2))
;; 100 200

Set’s destructuring

Sets are just like maps, but the key and value are set to the same value. This it can be useful to test whether an element is part of a set.

(let [{:strs [blue white black]} #{"blue" "white" "red" "green"}]
  (println blue white black))
;; blue white nil

Set’s destructuring can be useful when you have a function which can optionally accepts flags to modify its behaviour. For example let’s consider the an hypothetical function ls which behave like the unix command /bin/ls this function takes a path and an optional set of flags.

(defn ls [path & flags]
  (let [{:keys [all long-format human-readable sort-by-time]} (set flags)]
    ;; now you can test your flags
    (when long-format (comment do someting))
    ;; ....
    ))

Notice that we are creating a Clojure set out of the flags sequence, and then using destructuring to capture the individual flags.

Destructuring namespaced keys

If your maps have namespaced keys then the destructuring forms have to be slightly changed. Given the following map:

;; namespaced keys
(def contact
  {:firstname          "John"
   :lastname           "Smith"
   :age                25
   :corporate/id       "LDF123"
   :corporate/position "CEO"})

;; notice how the namespaced `:corporate/position` is extracted
;; the symbol which is bound to the value has no namespace
(let [{:keys [lastname corporate/position]} contact]
  (println lastname "-" position))
;; Smith - CEO


;; like for normal keys, the vector of symbols can be
;; replaced with a vector of keywords
(let [{:keys [:lastname :corporate/position]} contact]
  (println lastname "-" position))
;; Smith - CEO


;; Clojure 1.9 and subsequent releases
;; a default value might be provided
(let [{:keys [lastname corporate/position]
       :or {position "Employee"}} contact]
  (println lastname "-" position))

;; Clojure 1.8 or previous (doesn't work on CLJ 1.9+)
;; a default value might be provided
(let [{:keys [lastname corporate/position]
       :or {corporate/position "Employee"}} contact]
  (println lastname "-" position))
;; Smith - CEO

Notice in this last example there is change between Clojure up to 1.8 and Clojure 1.9. In Clojure 1.9 the default key for a namespaced key must be without the namespace otherwise you get a compilation exception. Same applies on the next example.

Finally a reminder that double-colon :: is a shortcut to represent current namespace.


(def contact
  {:firstname "John"
   :lastname  "Smith"
   :age       25
   ::id       "LDF123"
   ::position "CEO"})

;; Clojure 1.9
(let [{:keys [lastname ::position]
       :or {position "Employee"}} contact]
  (println lastname "-" position))
;; Smith - CEO

;; Clojure 1.8 or previous
(let [{:keys [lastname ::position]
       :or {::position "Employee"}} contact]
  (println lastname "-" position))
;; Smith - CEO

Obviously, same rule applied to symbols when used as keys:

(def contact
  {'firstname          "John"
   'lastname           "Smith"
   'age                25
   'corporate/id       "LDF123"
   'corporate/position "CEO"})

(let [{:syms [lastname corporate/position]} contact]
  (println lastname "-" position))
;; Smith - CEO

We can combine the different types of destructuring to create powerful and declarative functions.

(def contact
  {:firstname          "John"
   :lastname           "Smith"
   :age                25
   :corporate/id       "LDF123"
   :corporate/position "CEO"})


(defn contact-line
  ;; map destructuring
  [{:keys [firstname lastname corporate/position] :as contact}]
  ;; seq destructuring
  (let [initial firstname]
    (str "Mr " initial ". " lastname ", " position)))

(contact-line contact)
;;=> "Mr John. Smith, CEO"

Finally, namespaces keys can be destructured also by prepending the :keys keyword with the namespace they keys should be searched for. For example:

(def contact
  {:firstname          "John"
   :lastname           "Smith"
   :age                25
   :corporate/id       "LDF123"
   :corporate/position "CEO"
   :corporate/phone    "+1234567890"
   :personal/mobile    "0987654321"})

(let [{:keys [lastname]
       :corporate/keys [phone]
       :personal/keys [mobile]} contact]
  (println "Contact Mr." lastname "work:" phone " mobile:" mobile))
;; Contact Mr. Smith work: +1234567890  mobile: 0987654321

Destructuring “gotchas”.

Destructuring is a powerful tool to make your code simpler, however there are a couple of things you should look for and common mistakes which pass unnoticed by the compiler.

Typo in defaults

One mistake which is very hard to track and the compiler doesn’t give you any help with is when you put a typo in the default value keys (the :or part).

Notice how username is defined in the :keys part of destructuring while in the default values map (:or) I’ve used user-name. The compiler won’t complain, and the default value won’t be bound.

;; BAD DEFAULTS
(defn connect-db [host ; mandatory parameter
                  & {:keys [port db-name username password]
                     :or   {port     12345
                            db-name  "my-db"
                            user-name "db-user"
                            password "secret"}}]
  (println "connecting to:" host "port:" port "db-name:" db-name
           "username:" username "password:" password))

(connect-db "server")

;; connecting to: server port: 12345 db-name: my-db username: nil password: secret
;;                notice the username is `nil` ---------------^

Keywords in defaults (fixed in 1.9)

Similarly if you put keywords in the default’s map values won’t be bound and the compiler won’t complain up to Clojure 1.8. In Clojure 1.9 this is fixed, and a compilation error will be raised.

;; BAD DEFAULTS - Clojure 1.8 (doesn't work on CLJ 1.9+)
(defn connect-db [host ; mandatory parameter
                  & {:keys [port db-name username password]
                     :or   {:port     12345
                            :db-name  "my-db"
                            :username "db-user"
                            :password "secret"}}]
  (println "connecting to:" host "port:" port "db-name:" db-name
           "username:" username "password:" password))

(connect-db "server")

;; connecting to: server port: nil db-name: nil username: nil password: nil
;; notice all defaults are `nil`

Defaults in a vector (fixed in 1.9+)

If by mistake you put all defaults in a vector, again, no error from the compiler (up to Clojure 1.8) and no value will be bound.

;; BAD DEFAULTS - Clojure 1.8 (doesn't work on CLJ 1.9+)
(defn connect-db [host ; mandatory parameter
                  & {:keys [port db-name username password]
                     :or   [port     12345
                            db-name  "my-db"
                            username "db-user"
                            password "secret"]}]
  (println "connecting to:" host "port:" port "db-name:" db-name
           "username:" username "password:" password))

(connect-db "server")
;; connecting to: server port: nil db-name: nil username: nil password: nil
;; notice all defaults are `nil`

Multi-matching values with namespaced keys

Namespaced keys are good way to not conflate multiple meaning to a single key, and by assigning different namespaces to a key you can give different meaning. However you need to be careful when destructuring namespaced keys in Clojure to never have the same key name destructured multiple times in the same structure.

For example let’s assume you have a map as follow:

(def value {:id "id"
            :fistname "John"
            :lastname "Smith"
            :customer/id "customer/id"})

The key :id might represent the id of that particular record while :customer/id might be the :id coming from the CRM system for the same person. Clearly they are two different IDs in Clojure.

But check this out, if you mix them both in a single destructuring form you might have unexpected behaviours.

;; nothing strange here
(let [{:keys [:id]} value] (println id))
;; id

;; nothing strange here
(let [{:keys [:customer/id]} value] (println id))
;; customer/id

;; KEY MIXUP - BAD
;; in the next two examples we attempt to destructure both keys
;; at the same time with two different namespaces.
;; NOTICE the one which appear later in the structure wins.
(let [{:keys [:id :customer/id]} value] (println id))
;; customer/id

(let [{:keys [:customer/id :id]} value] (println id))
;; id

Conclusion

As we seen, value destructuring is a powerful Clojure’s feature. It can eliminate loads of boilerplate and repetitions which, often, lead to bugs. In the writing of this post I’ve looked to the excellent Jay Fields’ post3, some of the Clojure’s documentation4, I do suggest whoever is looking for more examples to have a look to those links. At first, destructuring might seems to complicate the syntax and the readability, however once you master the syntax, you’ll see that the code becomes clearer and event more readable.

Clojure destructuring cheatsheet 

;; all the following destructuring forms can be used in any of the
;; Clojure's `let` derived bindings such as function's parameters,
;; `let`, `loop`, `binding`, `for`, `doseq`, etc.

;; list, vectors and sequences
[zero _ _ three & four-and-more :as numbers] (range)
;; zero = 0, three = 3, four-and-more = (4 5 6 7 ...),
;; numbers = (0 1 2 3 4 5 6 7 ...)

;; maps and sets
{:keys [firstname lastname] :as person} {:firstname "John"  :lastname "Smith"}
{:keys [:firstname :lastname] :as person} {:firstname "John"  :lastname "Smith"}
{:strs [firstname lastname] :as person} {"firstname" "John" "lastname" "Smith"}
{:syms [firstname lastname] :as person} {'firstname "John"  'lastname "Smith"}
;; firstname = John, lastname = Smith, person = {:firstname "John" :lastname "Smith"}

;; maps destructuring with different local vars names
{name :firstname family-name :lastname :as person} {:firstname "John"  :lastname "Smith"}
;; name = John, family-name = Smith, person = {:firstname "John" :lastname "Smith"}

;; default values
{:keys [firstname lastname] :as person
 :or {firstname "Jane"  :lastname "Bloggs"}} {:firstname "John"}
;; firstname = John, lastname = Bloggs, person = {:firstname "John"}

;; nested destructuring
[[x1 y1] [x2 y2] [_ _ z]]  [[2 3] [5 6] [9 8 7]]
;; x1 = 2, y1 = 3, x2 = 5, y2 = 6, z = 7

{:keys [firstname lastname]
    {:keys [phone]} :contact} {:firstname "John" :lastname "Smith" :contact {:phone "0987654321"}}
;; firstname = John, lastname = Smith, phone = 0987654321

;; namespaced keys in maps and sets
{:keys [contact/firstname contact/lastname] :as person}     {:contact/firstname "John" :contact/lastname "Smith"}
{:keys [:contact/firstname :contact/lastname] :as person}   {:contact/firstname "John" :contact/lastname "Smith"}
{:keys [::firstname ::lastname] :as person}                 {::firstname "John"        ::lastname "Smith"}
{:syms [contact/firstname contact/lastname] :as person}     {'contact/firstname "John"     'contact/lastname "Smith"}
;; firstname = John, lastname = Smith, person = {:firstname "John" :lastname "Smith"}


For this article I’ve used:

  • Clojure 1.6.0, 1.7.0, 1.8.0, 1.9.0, 1.10.0 and 1.10.1

Updates:

  • 2017-05-13 - added destructuring of namespaced keys.
  • 2017-05-23 - added common mistakes / gotchas.
  • 2018-02-22 - added more gotchas
  • 2018-05-15 - updated to Clojure 1.9
  • 2018-12-18 - updated to Clojure 1.10
  • 2019-01-03 - added :namespaced/keys extraction.
  • 2019-07-23 - updated to Clojure 1.10.1

References:

comments powered by Disqus