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:
- Clojure Functions - Working with higher-order functions
- Sequences - Lazy sequences and sequence processing
- Macros - Extending the language with macros