CodeToLive

Clojure Data Structures

Clojure provides a rich set of immutable, persistent data structures that are central to its functional programming paradigm. These structures are efficient and share common interfaces.

Core Data Structures

Clojure has four primary collection types:

1. Lists


; List literal - note the quote
'(1 2 3 4)      ; => (1 2 3 4)

; Creating with list function
(list 1 2 3 4)  ; => (1 2 3 4)

; Characteristics:
; - Singly linked list
; - Efficient addition/removal at head
; - O(n) access by index
; - Evaluated as function calls if unquoted

; Operations
(cons 0 '(1 2 3))   ; => (0 1 2 3) - add to head
(conj '(1 2 3) 0)   ; => (0 1 2 3) - add to head
(first '(1 2 3))    ; => 1
(rest '(1 2 3))     ; => (2 3)
(nth '(1 2 3) 1)    ; => 2 (O(n) operation)
                

2. Vectors


; Vector literal
[1 2 3 4]       ; => [1 2 3 4]

; Creating with vector function
(vector 1 2 3 4) ; => [1 2 3 4]

; Characteristics:
; - Indexed access in O(1) time
; - Efficient addition/removal at end
; - Tree structure with 32-way branching

; Operations
(conj [1 2 3] 4)    ; => [1 2 3 4] - add to end
(get [1 2 3] 1)     ; => 2
([1 2 3] 1)         ; => 2 (same as above)
(nth [1 2 3] 1)     ; => 2
(subvec [1 2 3 4] 1 3) ; => [2 3]
                

3. Maps


; Map literal
{:name "Alice" :age 30}   ; => {:name "Alice", :age 30}

; Creating with hash-map function
(hash-map :name "Alice" :age 30) ; => {:age 30, :name "Alice"}

; Characteristics:
; - Key-value pairs
; - Keys can be any immutable type
; - Implemented as hash array mapped tries

; Operations
(get {:a 1 :b 2} :a)     ; => 1
({:a 1 :b 2} :a)         ; => 1 (map as function)
(:a {:a 1 :b 2})         ; => 1 (keyword as function)
(assoc {:a 1} :b 2)      ; => {:a 1, :b 2}
(dissoc {:a 1 :b 2} :a)  ; => {:b 2}
(keys {:a 1 :b 2})       ; => (:a :b)
(vals {:a 1 :b 2})       ; => (1 2)
                

4. Sets


; Set literal
#{1 2 3}        ; => #{1 2 3}

; Creating with hash-set function
(hash-set 1 2 3) ; => #{1 2 3}

; Characteristics:
; - Unique elements
; - Unordered
; - Fast membership testing

; Operations
(conj #{1 2} 3)     ; => #{1 2 3}
(disj #{1 2 3} 2)   ; => #{1 3}
(contains? #{1 2 3} 2) ; => true
(get #{1 2 3} 2)     ; => 2
                

Persistent Data Structures

Clojure's data structures are persistent, meaning they preserve previous versions when modified. They are implemented using structural sharing:


(def v1 [1 2 3 4 5])
(def v2 (conj v1 6))

; v1 remains unchanged
v1  ; => [1 2 3 4 5]
v2  ; => [1 2 3 4 5 6]

; The structures share common parts in memory
                

Common Sequence Operations

All Clojure collections implement the sequence abstraction, so they share common operations:

map


(map inc [1 2 3])      ; => (2 3 4)
(map str ["a" "b" "c"] [1 2 3]) ; => ("a1" "b2" "c3")
                

filter


(filter even? [1 2 3 4 5]) ; => (2 4)
(filter #(> % 2) [1 2 3 4]) ; => (3 4)
                

reduce


(reduce + [1 2 3 4])   ; => 10
(reduce * 2 [1 2 3])   ; => 12 (with initial value 2)
                

into


(into [] '(1 2 3))     ; => [1 2 3] (list to vector)
(into {} [[:a 1] [:b 2]]) ; => {:a 1, :b 2}
                

Specialized Collections

Queues


; Create a persistent queue
(def q (conj clojure.lang.PersistentQueue/EMPTY 1 2 3))

; Operations
(peek q)       ; => 1 (front element)
(pop q)        ; => queue with 1 removed
(conj q 4)     ; => queue with 4 added to end
                

Sorted Collections


; Sorted set
(def s (sorted-set 3 1 2)) ; => #{1 2 3}

; Sorted map
(def m (sorted-map :b 2 :a 1)) ; => {:a 1, :b 2}

; With custom comparator
(sorted-set-by > 1 2 3) ; => #{3 2 1}
                

Records


; Define a record type
(defrecord Person [name age])

; Create an instance
(def p (->Person "Alice" 30))

; Access fields
(:name p)      ; => "Alice"
(.name p)      ; => "Alice" (Java interop style)
                

Transients

For performance-critical code, Clojure provides mutable versions of its collections:


; Convert to transient
(def t (transient []))

; Mutate transient
(def t1 (conj! t 1))
(def t2 (conj! t1 2))

; Convert back to persistent
(persistent! t2) ; => [1 2]
                

Collection Patterns

Destructuring


; Vector destructuring
(let [[a b & rest] [1 2 3 4]]
  [a b rest])   ; => [1 2 (3 4)]

; Map destructuring
(let [{:keys [name age]} {:name "Alice" :age 30}]
  [name age])   ; => ["Alice" 30]

; Nested destructuring
(let [[a [b c] & rest] [1 [2 3] 4 5]
  [a b c rest]) ; => [1 2 3 (4 5)]
                

Threading Macros


; Thread-first (->)
(-> {:a 1}
    (assoc :b 2)
    (update :a inc)) ; => {:a 2, :b 2}

; Thread-last (->>)
(->> [1 2 3]
     (map inc)
     (filter even?)) ; => (2 4)
                

Performance Characteristics

Operation List Vector Map Set
conj O(1) O(1)* O(1)* O(1)*
peek O(1) O(1) - -
get by key - O(1) O(1)* O(1)*
count O(1) O(1) O(1) O(1)

* Effectively O(1) due to the 32-way branching tree structure

Choosing the Right Collection

  • Lists: When you need to add/remove from the front frequently
  • Vectors: General purpose indexed collections (default choice)
  • Maps: Key-value lookups and associations
  • Sets: When you need uniqueness or membership testing
  • Queues: When you need FIFO semantics

Next Steps

Continue learning with: