Clojure destructuring is broken up into two categories, sequential destructuring and associative destructuring. Sequential destructuring represents a sequential data structure as a Clojure vector within a let binding.
This type of destructuring can be used on any kind of data structure that can be traversed in linear time, including lists, vectors, seqs, strings, arrays, and anything that supports nth
.
(def my-vector [1 2 3])
(def my-list '(1 2 3))
(def my-string "abc")
;= It should come as no surprise that this will print out 1 2 3
(let [[x y z] my-vector]
(println x y z))
;= 1 2 3
;= We can also use a similar technique to destructure a list
(let [[x y z] my-list]
(println x y z))
;= 1 2 3
;= For strings, the elements are destructured by character.
(let [[x y z] my-string]
(println x y z)
(map type [x y z]))
;= a b c
;= (java.lang.Character java.lang.Character java.lang.Character)
The key to sequential destructuring is that you bind the values one-by-one to the symbols in the vector. For instance the vector [x y z]
will match each element one-by-one with the list '(1 2 3)
.
In some cases, the collection you are destructuring isn’t the exact same size as the destructuring bindings. If the vector is too small, the extra symbols will be bound to nil. In general, if the binding form does not match a value, the binding will be nil.
(def small-list '(1 2 3))
(let [[a b c d e f g] small-list]
(println a b c d e f g))
;= 1 2 3 nil nil nil nil
On the other hand, if the collection is too large, the extra values are simply ignored.
(def large-list '(1 2 3 4 5 6 7 8 9 10))
(let [[a b c] large-list]
(println a b c))
;= 1 2 3
Destructuring gives you total control over the elements that you choose to bind (or not) and how you bind them.
Many times, you don’t need access to every element in a collection, only certain ones.
(def names ["Michael" "Amber" "Aaron" "Nick" "Earl" "Joe"])
Say you want to print the first element on one line and the remainder on another line.
(let [[item1 item2 item3 item4 item5 item6] names]
(println item1)
(println item2 item3 item4 item5 item6))
;= Michael
;= Amber Aaron Nick Earl Joe
This binding works but even using destructuring it’s pretty clunky. Instead we can use &
to combine the tail elements into a sequence.
(let [[item1 & remaining] names]
(println item1)
(apply println remaining))
;= Michael
;= Amber Aaron Nick Earl Joe
You can ignore bindings that you don’t intend on using by binding them to any symbol of your choosing.
(let [[item1 _ item3 _ item5 _] names]
(println "Odd names:" item1 item3 item5))
;= Odd names: Michael Aaron Earl
The convention for this is to use an underscore like above.
You can use :as all
to bind the entire vector to the symbol all
.
(let [[item1 :as all] names]
(println "The first name from" all "is" item1))
;= The first name from [Michael Amber Aaron Nick Earl Joe] is Michael
Let’s stop for a bit and look a little further into the types of :as
and &
.
(def numbers [1 2 3 4 5])
(let [[x & remaining :as all] numbers]
(apply prn [remaining all]))
;= (2 3 4 5) [1 2 3 4 5]
Here remaining
is bound to a sequence containing the remaining elements of the numbers
vector while all
has been bound to the original vector
. What happens when we destructure a string instead?
(def word "Clojure")
(let [[x & remaining :as all] word]
(apply prn [x remaining all]))
;= \C (\l \o \j \u \r \e) "Clojure"
Here all
is bound to the original structure (String, vector, list, whatever it may be) and x
is bound to the character \C
, and remaining
is the remaining list of characters.
You can combine any or all of these techniques at the same time at your discretion.
(def fruits ["apple" "orange" "strawberry" "peach" "pear" "lemon"])
(let [[item1 _ item3 & remaining :as all-fruits] fruits]
(println "The first and third fruits are" item1 "and" item3)
(println "These were taken from" all-fruits)
(println "The fruits after them are" remaining))
;= The first and third fruits are apple and strawberry
;= These were taken from [apple orange strawberry peach pear lemon]
;= The fruits after them are (peach pear lemon)
Destructuring can also be nested to get access to arbitrary levels of sequential structure. Let’s go back to our vector from the very beginning, my-line
.
(def my-line [[5 10] [10 20]])
This vector is comprised of nested vectors that we can access directly.
(let [[[x1 y1][x2 y2]] my-line]
(println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"
When you have nested vectors, you can use :as
or &
at any level as well.
(let [[[a b :as group1] [c d :as group2]] my-line]
(println a b group1)
(println c d group2))
;= 5 10 [5 10]
;= 10 20 [10 20]