Persistent, Immutable Collections
Every core Clojure collection — vectors, lists, maps, and sets — is immutable: once created, a collection's contents never change. Operations that appear to 'modify' a collection, such as conj, assoc, or dissoc, always return a brand-new collection and leave the original untouched. This is called a persistent data structure, because old versions of the collection remain valid and accessible even after new versions are created. For example, (def v1 [1 2 3]) followed by (def v2 (conj v1 4)) leaves v1 as [1 2 3] while v2 becomes [1 2 3 4].
Cricket analogy: conj creating a new vector instead of mutating v1 is like a scorer never erasing the over-by-over card from the 10th over of a Cricket World Cup final — each new ball's outcome is recorded as a fresh entry, so the original 10th-over record stays intact forever.
Structural Sharing
If every 'change' created a full copy, immutability would be prohibitively slow for large collections. Clojure avoids this with structural sharing: internally, vectors and maps are implemented as tries (branching tree structures), so conj or assoc only needs to copy the small path of nodes from the root down to the changed leaf, while every unchanged branch is shared between the old and new versions. This gives near-constant-time performance for common operations even though the collection is logically immutable.
Cricket analogy: Structural sharing is like a team only updating the specific batting-order slot that changed on the electronic scoreboard rather than reprinting the entire scorecard from scratch every time a wicket falls in an IPL match.
Vectors and Lists: Choosing the Right Sequential Structure
Clojure has two primary sequential collections. Lists, written (1 2 3) or built with list, are linked structures optimized for adding and removing elements at the front with conj and cons — they are also what Clojure code itself is made of, since a function call like (+ 1 2) is literally a list. Vectors, written [1 2 3], are indexed structures optimized for adding elements at the end with conj and for fast random access with get or nth — (nth [10 20 30] 1) returns 20 in effectively constant time, whereas indexing into a list requires walking from the front.
Cricket analogy: Choosing a vector for indexed access is like a scorer instantly looking up 'who faced ball 47 of the innings' from a numbered ball-by-ball array, whereas a list is more like a stack of handwritten notes where finding ball 47 means flipping through from ball 1.
Maps and Sets
Clojure's hash-map associates keys with values and provides average constant-time lookup: (get {:name "Ada" :lang "Clojure"} :lang) returns "Clojure". sorted-map maintains keys in sorted order at the cost of logarithmic-time operations. Sets, created with hash-set or the #{} literal, store unique unordered elements and support contains? for constant-time membership tests: (contains? #{:a :b :c} :b) returns true. Because maps and sets are themselves immutable, they can safely be used as keys or values inside other maps and sets without fear of the nested structure changing underneath you.
Cricket analogy: A hash-map is like an ICC player database where you look up Virat Kohli's average by name directly, while a sorted-map is like the official batting-average leaderboard that always stays ranked from highest to lowest as new innings are added.
(def team {:name "Alice" :role "Engineer" :skills #{:clojure :datomic}})
;; "Modifying" returns a new map; the original is untouched
(def team2 (assoc team :role "Senior Engineer"))
(:role team) ;=> "Engineer"
(:role team2) ;=> "Senior Engineer"
;; Vectors: fast indexed access
(def scores [88 95 76 91])
(nth scores 2) ;=> 76
(conj scores 100) ;=> [88 95 76 91 100]
scores ;=> [88 95 76 91] (unchanged)
;; Sets: unique, unordered membership
(def tags #{:beginner :clojure :functional})
(contains? tags :clojure) ;=> true
(conj tags :clojure) ;=> #{:beginner :clojure :functional} (no duplicate added)
Because Clojure's core collections are immutable, they are automatically safe to share across threads without locks — there is no risk of one thread seeing a partially-updated vector or map while another thread is writing to it. This is a foundational reason Clojure is well suited to concurrent programming.
Immutable updates like assoc and conj always return a new collection — they do not modify the original in place. A common beginner mistake is calling (conj my-vector 4) and expecting my-vector itself to change; you must capture the return value, e.g. (def my-vector (conj my-vector 4)).
- All of Clojure's core collections — lists, vectors, maps, sets — are immutable and persistent: 'updates' return new versions rather than mutating in place.
- Structural sharing (via trie-based internal structures) lets persistent collections perform near-constant-time updates without copying the entire structure.
- Lists are optimized for adding/removing at the front and are the structure Clojure code itself is made of.
- Vectors are optimized for adding at the end and for fast indexed access via nth or get.
- Hash-maps give average constant-time key lookup; sorted-maps maintain key order at logarithmic cost.
- Sets store unique, unordered elements and support constant-time membership checks with contains?.
- Immutability makes Clojure collections inherently safe to share across threads without locks.
Practice what you learned
1. What happens to the original vector when you call (conj v 4)?
2. Which structure is Clojure optimized for fast indexed access (nth)?
3. What technique allows persistent collections to avoid copying the entire structure on every update?
4. What does (contains? #{:a :b} :b) return?
5. Why are Clojure's immutable collections considered thread-safe by default?
Was this page helpful?
You May Also Like
Sequences in Clojure
Understand Clojure's sequence abstraction — the uniform interface of first, rest, and cons that lets the same functions work across lists, vectors, maps, and lazy, potentially infinite streams.
Functions in Clojure
Learn how to define, call, and compose functions in Clojure, from simple defn forms to anonymous functions, multi-arity definitions, and higher-order functions.
Destructuring in Clojure
Learn how Clojure's destructuring syntax lets you pull values out of vectors and maps directly in let bindings and function parameters, making data-heavy code concise and readable.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics