dissoc

Destructuring with computed keys

In Clojure you can destructure a map using an arbitrary expression as the key. For example, here kw is a local binding.

(let [kw :key
      {a kw} {:key 1}]
  a)
;=> 1

Usually this syntax is demonstrated as {sym0 :kw0 sym1 :kw1 ...}, which doesn’t reveal that the keywords are actually in expression position, or an evaluation context. The reason why this more recognizable syntax works is because keyword literals are self-evaluating.

(let [{a :key} {:key 1}]
  a)
;=> 1

The basic rule for expanding these expressions is:

(let [{binding expression} map])
=>
(let [binding (get map expression)])

So that code is equivalent to:

(let [a (get {:key 1} :key)]
  a)

Symbols are not self-evaluating syntax in Clojure, so they must be quoted:

(let [{a 'key} {'key 1}]
  a)
;=> 1

Applying the rule makes the need more obvious:

(let [a (get {'key 1} 'key)]
  a)

Destructuring is pleasingly compositional. This ability to drop down to computed keys makes destructuring available in many more situations than if all keys were required to be statically declared. I found a few examples in my own code and other libraries where this flexibility has been useful.

An example that dereferences the var u/expr-type to compute the key:

(let [{cargs :args
       res u/expr-type} (-> expr-noinline
                            ana2/unmark-top-level
                            ana2/unmark-eval-top-level
                            (check-expr expected opts))]

Another example that uses three class literals as computed keys:

(let [r (reflect-u/reflect cls)
      {methods clojure.reflect.Method
       fields clojure.reflect.Field
       ctors clojure.reflect.Constructor
       :as members}
      (group-by
        class
        (filter (fn [{:keys [name] :as m}] 
                  (if constructor-call
                    (instance? clojure.reflect.Constructor m)
                    (= m-or-f name)))
                (:members r)))]

A snippet of code that destructures nested maps using a mix of keywords, quoted symbols and computed vectors-of-locals as keys. Notice that the vectors are in binding position sometimes to introduce names, then in expression position to perform lookups.


(let [...
      {{[x1 x2] 'x} :fv
       {[y1] 'y [z1 z2] 'z} :idx} remap
      {{{[y1_x1 y1_x2] 'x
         [y1_y1 y1_y2 y1_y3 y1_y4] 'y} [y1]
        {[z1_x1] 'x
         [z1_y1] 'y} [y1 z1]
        {[z2_x1] 'x
         [z2_y1] 'y} [y1 z2]} :idx-context} remap]
  (is (= {:fv {'x [x1 x2]}
          :idx {'y [y1]
                'z [z1 z2]}
          :idx-context {[y1] {'x [y1_x1 y1_x2]
                              'y [y1_y1 y1_y2 y1_y3 y1_y4]}
                        [y1 z1] {'x [z1_x1]
                                 'y [z1_y1]}
                        [y1 z2] {'x [z2_x1]
                                 'y [z2_y1]}}}
         remap)))

You’ve probably seen code like this that destructures booleans from a group-by:

(let [...
      {anns false inits true} (group-by list? normalised-bindings)]

This Malli snippet nests :keys destructuring under a local binding key, method:


(-value-transformer [_ schema method options]
  (reduce
   (fn [acc {{:keys [name qname default transformers]} method}]

And this example elegantly destructures a nested map using keywords and locals, supporting the common pattern of updating a nested value in an atom then destructuring the swapped-in value’s relevant parts.


(defn remove-stale-cache-entries
  [nsym ns-form-str sforms slurped opts]
  {:pre [(simple-symbol? nsym)]}
  (when ns-form-str
    (let [{{{forms-cache ns-form-str} nsym} ::check-form-cache}
          (env/swap-checker!
            (env/checker opts)
            update-in
            [::check-form-cache nsym]
            (fn [m]
              (some-> m
                      (select-keys [ns-form-str])
                      not-empty
                      (update ns-form-str select-keys sforms))))]

07 Jul 2026