How Clojure uses persistent collections and structural sharing to make immutable updates practical for concurrency and everyday programming.
Immutable data structures are one of the reasons Clojure feels different from many mainstream languages. The important detail is that Clojure does not make immutability practical by copying whole collections on every change. Instead, its core collections are persistent data structures: updates create a new logical version while reusing most of the existing structure.
That model gives you two benefits at once. You get the reasoning advantages of immutable values, and you avoid the performance disaster that a naive full-copy strategy would cause. The rest of the page is really about that trade-off.
Immutability refers to the inability to change an object after it has been created. In contrast to mutable objects, which can be modified after their creation, immutable objects remain constant. This concept is pivotal in functional programming, where functions are expected to produce the same output given the same input, without side effects.
Immutability is crucial in functional programming for several reasons:
Clojure implements immutability at the level of its core collections. Lists, vectors, maps, and sets are designed to support efficient read and update operations without mutating the original value. The key implementation idea is structural sharing.
Structural sharing means that when a new version of a collection is created, only the path that actually changed needs to be rebuilt. The rest of the structure is reused. That minimizes allocation pressure and keeps immutable updates practical.
1;; Example of structural sharing with vectors
2(def original-vector [1 2 3 4])
3(def updated-vector (assoc original-vector 1 42))
4
5;; original-vector remains unchanged
6;; updated-vector reuses most of original-vector
In this example, assoc does not rebuild the whole vector from scratch. It creates a new logical version with the changed slot, while reusing the untouched parts of the original vector. The exact internals are more sophisticated than a toy tree, but the core idea is the same.
Immutability offers several advantages, particularly in the context of concurrent programming and code safety.
Immutable data structures are inherently thread-safe. Since they cannot be modified, multiple threads can access them simultaneously without the risk of data corruption or race conditions.
1;; Example of thread-safe access to immutable data
2(def shared-data [1 2 3 4])
3
4(future (println "Thread 1: " (conj shared-data 5)))
5(future (println "Thread 2: " (conj shared-data 6)))
6
7;; Both threads can safely access shared-data
In this example, both threads can safely access shared-data without any synchronization mechanisms, as the data is immutable.
With immutable data, the state of the data is predictable and consistent. This makes it easier to reason about the behavior of programs, as the data does not change unexpectedly.
1;; Example of predictable behavior with immutable data
2(defn add-element [coll element]
3 (conj coll element))
4
5(def original-list [1 2 3])
6(def new-list (add-element original-list 4))
7
8;; original-list remains unchanged
9;; new-list is a new version with the added element
Here, original-list remains unchanged, and new-list is a new version with the added element. This predictability simplifies understanding and debugging code.
Clojure provides several built-in immutable data structures, each with its own characteristics and use cases.
Lists in Clojure are immutable and singly linked. They are ideal for scenarios where you need to frequently add or remove elements from the front.
1;; Example of using immutable lists
2(def my-list '(1 2 3))
3(def new-list (cons 0 my-list))
4
5;; my-list remains unchanged
6;; new-list has 0 added to the front
Vectors are indexed, immutable collections that provide efficient random access and updates. They are suitable for scenarios where you need to access elements by index.
1;; Example of using immutable vectors
2(def my-vector [1 2 3])
3(def updated-vector (assoc my-vector 1 42))
4
5;; my-vector remains unchanged
6;; updated-vector has the second element changed to 42
Maps are key-value pairs that are immutable. They are useful for representing associative data.
1;; Example of using immutable maps
2(def my-map {:a 1 :b 2})
3(def updated-map (assoc my-map :c 3))
4
5;; my-map remains unchanged
6;; updated-map has a new key-value pair :c 3
Sets are collections of unique elements that are immutable. They are ideal for scenarios where you need to ensure uniqueness.
1;; Example of using immutable sets
2(def my-set #{1 2 3})
3(def updated-set (conj my-set 4))
4
5;; my-set remains unchanged
6;; updated-set has the new element 4 added
In many programming languages, data structures are mutable by default. This means they can be changed after creation, which can lead to issues in concurrent programming and make reasoning about code more complex.
In languages like Java or Python, data structures such as lists or arrays are typically mutable. This allows for in-place modifications, which can be efficient but also introduces the risk of unintended side effects.
1// Example of mutable list in Java
2List<Integer> myList = new ArrayList<>(Arrays.asList(1, 2, 3));
3myList.add(4); // Modifies the original list
In this Java example, myList is modified in place, which can lead to issues if the list is shared across different parts of a program.
The example below uses a deliberately tiny tree so the sharing pattern is easy to see. Real persistent vectors use a much larger branching factor, but the important idea is unchanged: the updated path is copied, while untouched branches are shared.
What to notice:
That is why immutable updates are cheap enough to use as the default mental model in Clojure.
Experiment with the following code examples to deepen your understanding of immutable data structures in Clojure. Try modifying the examples to see how immutability affects the behavior of your code.
assoc to update a map and compare the original and updated maps.To reinforce your understanding of immutable data structures, try answering the following questions.