Skip to content

Instantly share code, notes, and snippets.

@john2x
Last active August 23, 2024 07:45
Show Gist options
  • Save john2x/e1dca953548bfdfb9844 to your computer and use it in GitHub Desktop.
Save john2x/e1dca953548bfdfb9844 to your computer and use it in GitHub Desktop.
Clojure Destructuring Tutorial and Cheat Sheet

Clojure Destructuring Tutorial and Cheat Sheet

(Related blog post)

Simply put, destructuring in Clojure is a way extract values from a datastructure and bind them to symbols, without having to explicitly traverse the datstructure. It allows for elegant and concise Clojure code.

Vectors and Sequences

Syntax: [symbol another-symbol] ["value" "another-value"]

(def my-vector [:a :b :c :d])
(def my-nested-vector [:a :b :c :d [:x :y :z]])

(let [[a b c d] my-vector]
  (println a b c d))
;; => :a :b :c :d

(let [[a _ _ d [x y z]] my-nested-vector]
  (println a d x y z))
;; => :a :d :x :y :z

You don't have to match the full vector.

(let [[a b c] my-vector]
  (println a b c))
;; => :a :b :c

You can use & the-rest to bind the remaining part of the vector to the-rest.

(let [[a b & the-rest] my-vector]
  (println a b the-rest))
;; => :a :b (:c :d)

When a destructuring form "exceeds" a vector (i.e. there not enough items in the vector to bind to), the excess symbols will be bound to nil.

(let [[a b c d e f g] my-vector]
  (println a b c d e f g))
;; => :a :b :c :d nil nil nil

You can use :as some-symbol as the last two items in the destructuring form to bind the whole vector to some-symbol

(let [[:as all] my-vector]
  (println all))
;; => [:a :b :c :d]

(let [[a :as all] my-vector]
  (println a all))
;; => :a [:a :b :c :d]

(let [[a _ _ _ [x y z :as nested] :as all] my-nested-vector]
  (println a x y z nested all))
;; => :a :x :y :z [:x :y :z] [:a :b :c :d [:x :y :z]]

You can use both & the-rest and :as some-symbol.

(let [[a b & the-rest :as all] my-vector]
  (println a b the-rest all))
;; => :a :b (:c :d) [:a :b :c :d]

:as and & the-rest work for both vectors and sequences (including lists). :as preserves them (as a list, or as a vector):

(def short-vector    [1 2])
(def short-sequence '(1 2))
(let [[:as x] short-vector  ] x) ;=> [1 2]
(let [[:as x] short-sequence] x) ;=> (1 2)

However, & the-rest doesn't preserve a vector, but it converts a vector into a list:

(let [[& a] short-sequence] a) ;=> (1 2)
(let [[& a] short-vector  ] a) ;=> (1 2) - not preserved!

Optional arguments for functions

With destructuring and the & the-rest form, you can specify optional arguments to functions.

(defn foo [a b & more-args]
  (println a b more-args))
(foo :a :b) ;; => :a :b nil
(foo :a :b :x) ;; => :a :b (:x)
(foo :a :b :x :y :z) ;; => :a :b (:x :y :z)

(defn foo [a b & [x y z]]
  (println a b x y z))
(foo :a :b) ;; => :a :b nil nil nil
(foo :a :b :x) ;; => :a :b :x nil nil
(foo :a :b :x :y :z) ;; => :a :b :x :y :z

Maps

Syntax: {symbol :key, another-symbol :another-key} {:key "value" :another-key "another-value"}

(def my-hashmap {:a "A" :b "B" :c "C" :d "D"})
(def my-nested-hashmap {:a "A" :b "B" :c "C" :d "D" :q {:x "X" :y "Y" :z "Z"}})

(let [{a :a d :d} my-hashmap]
  (println a d))
;; => A D

(let [{a :a, b :b, {x :x, y :y} :q} my-nested-hashmap]
  (println a b x y))
;; => A B X Y

Similar to vectors, if a key is not found in the map, the symbol will be bound to nil.

(let [{a :a, not-found :not-found, b :b} my-hashmap]
  (println a not-found b))
;; => A nil B

You can provide an optional default value for these missing keys with the :or keyword and a map of default values.

(let [{a :a, not-found :not-found, b :b, :or {not-found ":)"}} my-hashmap]
  (println a not-found b))
;; => A :) B

The :as some-symbol form is also available for maps, but unlike vectors it can be specified anywhere (but still preferred to be the last two pairs).

(let [{a :a, b :b, :as all} my-hashmap]
  (println a b all))
;; => A B {:a A :b B :c C :d D}

And combining :as and :or keywords (again, :as preferred to be the last).

(let [{a :a, b :b, not-found :not-found, :or {not-found ":)"}, :as all} my-hashmap]
  (println a b not-found all))
;; => A B :) {:a A :b B :c C :d D}

There is no & the-rest for maps.

Shortcuts

Having to specify {symbol :symbol} for each key is repetitive and verbose (it's almost always going to be the symbol equivalent of the key), so shortcuts are provided so you only have to type the symbol once.

Here are all the previous examples using the :keys keyword followed by a vector of symbols:

(let [{:keys [a d]} my-hashmap]
  (println a d))
;; => A D

(let [{:keys [a b], {:keys [x y]} :q} my-nested-hashmap]
  (println a b x y))
;; => A B X Y

(let [{:keys [a not-found b]} my-hashmap]
  (println a not-found b))
;; => A nil B

(let [{:keys [a not-found b], :or {not-found ":)"}} my-hashmap]
  (println a not-found b))
;; => A :) B

(let [{:keys [a b], :as all} my-hashmap]
  (println a b all))
;; => A B {:a A :b B :c C :d D}

(let [{:keys [a b not-found], :or {not-found ":)"}, :as all} my-hashmap]
  (println a b not-found all))
;; => A B :) {:a A :b B :c C :d D}

There are also :strs and :syms alternatives, for when your map has strings or symbols for keys (instead of keywords), respectively.

(let [{:strs [a d]} {"a" "A", "b" "B", "c" "C", "d" "D"}]
  (println a d))
;; => A D

(let [{:syms [a d]} {'a "A", 'b "B", 'c "C", 'd "D"}]
  (println a d))
;; => A D

Keyword arguments for function

Map destructuring also works with sequences, including lists (but not with vectors).

(let [{:keys [a b]} '("X", "Y", :a "A", :b "B")]
(println a b))
;; => A B

(let [{:as m} '(:a "A", :b "B")]
(println m))
;; => {:b B, :a A}

This allows your functions to have optional pairs of keyword-based arguments.

(defn foo [a b & {:keys [x y]}]
  (println a b x y))
(foo "A" "B")  ;; => A B nil nil
(foo "A" "B" :x "X")  ;; => A B X nil
(foo "A" "B" :x "X" :y "Y")  ;; => A B X Y

It can also convert an alternating list of even numbers of varargs to a map.

(defn foo [& {:as m}]
  (println m))
(foo :x "X" :y "Y") ;; => {:y Y, :x X}

More examples

Here be dragons.

TODO

Compojure Destructuring

Official docs

The basic syntax of a Compojure route is as follows:

(REQUEST-METHOD "/path" request-data (handler-fn request-data))

The simplest form is to pass the entire Ring request map to your handler function.

Here, the request map will be bound to the request var and when my-handler is called it is passed as an argument. You can then extract/manipulate whatever data you want in the request map:

(defn my-handler [request] ...)
;;                   ___________________
;;                  |                   V
(GET "/some/path" request (my-handler request))

Since the request is just a regular Clojure map, Compojure allows you to destructure that map using the same map destructuring methods above. This is useful if you only want specific values from the map for the handler function to work with:

(defn my-handler [uri query-string]
  (println uri)
  ;; note that query-string is raw. e.g. "foo=bar&fizz=buzz"
  (println query-string))

(GET "/some/path" {uri :uri, query-string :query-string} (my-handler uri query-string))
;; or with the shortcuts
(GET "/some/path" {:keys [uri query-string]} (my-handler uri query-string))

If you want to pass the entire request map as well:

(defn my-handler [uri query-string request]
  (println request)
  (println uri)
  (println query-string))

(GET "/some/path" {:keys [uri query-string] :as request} (my-handler uri query-string request))

Compojure-specific Destructuring

Since regular destructuring can be quite verbose, Compojure offers a more specialised form of destructuring. If you supply a vector, Compojure will use this custom destructuring syntax 1.

Note: This only works for routes wrapped with the wrap-params middleware. This is because by default, a Ring request map doesn't include the :params entry (which contains the parsed value of :query-string). The wrap-params middleware parses the value of :query-string and inserts it into the request map as :params, which Compojure's special destructuring syntax targets. (TODO: Ring wiki is old. Is this still true?)

From here, assume that all handlers are wrapped with wrap-params.

(I got sleepy)

@cch1
Copy link

cch1 commented Mar 4, 2016

A useful pattern: say you have a function that takes an options map and you want to wrap it with a function that takes varargs and supplies defaults for a subset of the options. I struggled with this for a while and this is the best I could do:

((fn [& {:as options}]
    (let [options (merge {:defkey "defvalue"} options)]
       (apply original-function options))))

Any other thoughts on this? The :as options is critical as it coerces the varargs into a map when present. When not present, the merge is critical so that it can produce a map. In other words, without the need for defaults, there might be a better way. And if any of the varargs are mandatory there might be a better way. But for the general case, this is the best I could do.

@araoLiftoff
Copy link

Thanks!

@peter-lyons-kehl
Copy link

Dear John @john2x,

Thank you for this tutorial. In its fork https://gist.github.com/peter-kehl/e3953019327c7f0e116b7996cef20e16 I added examples on how to use :as in "Keyword arguments for function." Would you "pull" it, please.

@john2x
Copy link
Author

john2x commented Dec 4, 2018

Thanks @peter-kehl! I've applied your changes.

@dtonhofer
Copy link

Here's one that stumped me earlier:

If you destructure using a map (this is called "Associative Destructuring", sounds like something out of the psychotherapy handbook) ...

If the argument-to-be-destructured is a map (which is not a seq), then a lookup by key occurs:

(let [{firstthing :a, lastthing :b} {:a 1 :b 2}] [firstthing lastthing])
;=> [1 2]

(let [{firstthing :a, lastthing 3} {:a 1 3 8}] [firstthing lastthing])
;=> [1 8]

(let [{firstthing :b, lastthing 3} {:a 1 3 8}] [firstthing lastthing])
;=> [nil 8]

If the argument-to-be-destructured is a seq, then the seq is first poured into a map, and then a lookup by key occurs:

(let [{firstthing :a, lastthing 3} '(:a 1 3 8)] [firstthing lastthing])
;=> [1 8]

(let [{firstthing :a, lastthing :x} '(:a 1 3 8)] [firstthing lastthing])
;=> [1 nil]

(let [{firstthing :a, lastthing :x} '(:a 1 3)] [firstthing lastthing])
Execution error (IllegalArgumentException) at user/eval205 (REPL:1).
No value supplied for key: 3

If the argument-to-be-destructed is a vector (which is not a seq), then any integer used as lookup key get is used as an index into the vector, and anything not integer yields the lookup value nil (because get is used to extract the value):

(let [{firstthing 1, lastthing 3} [1 2 3 4]] [firstthing lastthing])
;=> [2 4]

(let [{firstthing 1, lastthing 3} [1 2 3]] [firstthing lastthing])
;=> [2 nil]

(let [{firstthing 1, lastthing :b} [1 2]] [firstthing lastthing])
;=> [2 nil]

(let [{firstthing 0, lastthing :b} [1 2]] [firstthing lastthing])
;=> [1 nil]

@douglasnaphas
Copy link

I have not been able to find where in the Clojure docs it says:

It [map destructuring] can also convert an alternating list of even numbers of varargs to a map.

(defn foo [& {:as m}]
  (println m))
(foo :x "X" :y "Y") ;; => {:y Y, :x X}

I am sure the docs support this statement, I am just not seeing how to interpret sections of the docs to figure out how & combines with map destructing to reach this result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment