Associative destructuring is similar to sequential destructuring, but applied instead to associative (key-value) structures (including maps, records, vectors, etc). The associative bindings are concerned with concisely extracting values of the map by key.
Let’s first consider an example that extracts values from a map without destructuring:
(def client {:name "Super Co."
:location "Philadelphia"
:description "The worldwide leader in plastic tableware."})
(let [name (:name client)
location (:location client)
description (:description client)]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
Note that each line of the let binding is essentially the same - it extracts a value from the map by the name of the key, then binds it to a local with the same name.
Below is a first example of doing the same thing with associative destructuring:
(let [{name :name
location :location
description :description} client]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
The destructuring form is now a map rather than a vector, and instead of a symbol on the left side of the let, we have a map. The keys of the map are the symbols we want to bind in the let. The values of the destructuring map are the keys we will look up in the associative value. Here they are keywords (the most common case), but they could be any key value - numbers, strings, symbols, etc.
Similar to sequential destructuring, if you try to bind a key that is not present in the map, the binding value will be nil.
(let [{category :category} client]
(println category))
;= nil
Associative destructuring, however, also allows you to supply a default value if the key is not present in the associative value with the :or
key.
(let [{category :category, :or {category "Category not found"}} client]
(println category))
;= Category not found
The value for :or
is a map where the bound symbol (here category
) is bound to the expression "Category not found"
. When category is not found in client
, it is instead found in the :or
map and bound to that value instead.
In sequential destructuring, you generally bind unneeded values with an _
. Since associative destructuring doesn’t require traversing the entire structure, you can simply omit any keys you don’t plan on using from the destructuring form.
If you need access to the entire map, you can use the :as
key to bind the entire incoming value, just as in sequential destructuring.
(let [{name :name :as all} client]
(println "The name from" all "is" name))
;= The name from {:name Super Co., :location Philadelphia, :description The world wide leader in plastic table-ware.} is Super Co.
The :as
and :or
keywords can be combined in a single destructuring.
(def my-map {:a "A" :b "B" :c 3 :d 4})
(let [{a :a, x :x, :or {x "Not found!"}, :as all} my-map]
(println "I got" a "from" all)
(println "Where is x?" x))
;= I got A from {:a "A" :b "B" :c 3 :d 4}
;= Where is x? Not found!
You might have noticed that our original example still contains redundant information (the local binding name and the key name) in the associative destructuring form. The :keys
key can be used to further remove the duplication:
(let [{:keys [name location description]} client]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
This example is exactly the same as the prior version - it binds name
to (:name client)
, location
to (:location client)
, and description
to (:description client)
.
The :keys
key is for associative values with keyword keys, but there are also :strs
and :syms
for string and symbol keys respectively. In all of these cases the vector contains symbols which are the local binding names.
(def string-keys {"first-name" "Joe" "last-name" "Smith"})
(let [{:strs [first-name last-name]} string-keys]
(println first-name last-name))
;= Joe Smith
(def symbol-keys {'first-name "Jane" 'last-name "Doe"})
(let [{:syms [first-name last-name]} symbol-keys]
(println first-name last-name))
;= Jane Doe
Associative destructuring can be nested and combined with sequential destructuring as needed.
(def multiplayer-game-state
{:joe {:class "Ranger"
:weapon "Longbow"
:score 100}
:jane {:class "Knight"
:weapon "Greatsword"
:score 140}
:ryan {:class "Wizard"
:weapon "Mystic Staff"
:score 150}})
(let [{{:keys [class weapon]} :joe} multiplayer-game-state]
(println "Joe is a" class "wielding a" weapon))
;= Joe is a Ranger wielding a Longbow
Keyword arguments
One special case is using associative destructuring for keyword-arg parsing. Consider a function that takes options :debug
and :verbose
. These could be specified in an options map:
(defn configure [val options]
(let [{:keys [debug verbose] :or {debug false, verbose false}} options]
(println "val =" val " debug =" debug " verbose =" verbose)))
(configure 12 {:debug true})
;;val = 12 debug = true verbose = false
However, it would be nicer to type if we could pass those optional arguments as just additional "keyword" arguments like this:
(configure 12 :debug true)
To support this style of invocation, associative destructuring also works with lists or sequences of key-value pairs for keyword argument parsing. The sequence comes from the rest arg of a variadic function but is destructured not with sequential destructuring, but with associative destructuring (so a sequence destructured as if it were the key-value pairs in a map):
(defn configure [val & {:keys [debug verbose]
:or {debug false, verbose false}}]
(println "val =" val " debug =" debug " verbose =" verbose))
(configure 10)
;;val = 10 debug = false verbose = false
(configure 5 :debug true)
;;val = 5 debug = true verbose = false
;; Note that any order is ok for the kwargs
(configure 12 :verbose true :debug true)
;;val = 12 debug = true verbose = true
The use of keyword arguments has fallen in and out of fashion in the Clojure community over the years. They are now mostly used when presenting interfaces that people are expected to type at the REPL or the outermost layers of an API. In general, inner layers of the code find it easier to pass options as an explicit map.
Namespaced keywords
If the keys in your map are namespaced keywords, you can also use destructuring with it, even though local binding symbols are not allowed to have namespaces. Destructuring a namespaced key will bind a value to the local name part of the key and drop the namespace.
(def human {:person/name "Franklin"
:person/age 25
:hobby/hobbies "running"})
(let [{:keys [hobby/hobbies]
:person/keys [name age]} human]
(println name "is" age "and likes" hobbies))
;= Franklin is 25 and likes running
Destructuring namespaced keywords using :keys
alone can result in local bindings that clash. Because all map destructuring options can be combined, any local binding form can be defined individually.
(def human {:person/name "Franklin"
:person/age 25
:hobby/name "running"})
(let [{:person/keys [age]
hobby-name :hobby/name
person-name :person/name} human]
(println person-name "is" age "and likes" hobby-name))
;= Franklin is 25 and likes running
You can even destructure using auto-resolved keywords, which will again be bound to only the name part of the key:
;; this assumes you have a person.clj namespace in your project
;; if not do the following at your repl instead: (create-ns 'person) (alias 'p 'person)
(require '[person :as p])
(let [person {::p/name "Franklin", ::p/age 25}
{:keys [::p/name ::p/age]} person]
(println name "is" age))
;= Franklin is 25
Creating and destructuring maps with auto-resolved keywords allow us to write code using a namespace alias (here p
) that is defined by a require
in the current namespace, giving us a means of namespace indirection that can be changed at a single place in the code.
All symbols bound in the context of destructuring can be further destructured - this allows destructuring to be used in a nested fashion for both sequential and associative destructuring. It is less obvious, but this also extends to the symbol defined after &
.
This example destructures the &
seq in place to decode the rest of the arguments as options (note that we are thus destructuring the two arguments sequentially and the rest associatively):
(defn f-with-options
[a b & {:keys [opt1]}]
(println "Got" a b opt1))
(f-with-options 1 2 :opt1 true)
;= Got 1 2 true