What is the difference between atom and ref in Clojure?

Key takeaways:

  • Atom is ideal for managing independent, isolated state with atomic updates using functions like swap! and reset!, ensuring thread safety.

  • Ref is designed for coordinated, transactional state updates across multiple references, ensuring consistency through Clojure's Software Transactional Memory (STM).

  • Both atom and ref are thread-safe, but they differ in use cases: atom is suited for independent state management, while ref excels in handling complex, coordinated state transformations.

Mutable state management is crucial for many applications, especially those involving concurrent or transactional operations. Programs can manage mutable states in Clojure programs by using the atom and ref techniques. While atom and ref are both used to manage mutable states, they are best suited for different situations and have unique qualities. Let’s have a look at each technique in detail.

Atom

An atom is a mutable referenceA mutable reference is a variable or a data structure whose value can be changed or updated after it is initially created. type designed to hold a single, immutable value. An atom’s value can be updated after it is generated, but the atom itself never changes. Updates to an atom are achieved using Clojure functions like swap!, reset!, or compare-and-set! which ensures atomicity and thread safetyThread safety refers to the property of a program, object, or data structure that guarantees correct behavior when accessed or modified by multiple threads concurrently (i.e., at the same time). In a multi-threaded environment, multiple threads can try to access or modify shared data simultaneously, which can lead to race conditions, data corruption, or inconsistent states if proper mechanisms aren’t in place.. Each atom operates independently, managing its own state without direct coordination with other atoms.

  • The atom is mutable because the reference it holds can be updated.

  • The value inside the atom is immutable, meaning once it's assigned, you can't change its internal structure, only replace it with a new value.

  • Functions like swap!, reset!, and compare-and-set! don't change the immutable value directly; instead, they change what the atom refers to, allowing for safe updates in a concurrent environment.

Use cases of an atom

The following are the typical applications for atoms:

  • Managing independent, isolated state: Atoms are well-suited for situations where separate pieces of mutable state need to be managed independently of each other.

  • Implementing caching mechanisms: Atoms can be employed to store precomputed values or outcomes of resource-intensive tasks, enhancing retrieval efficiency while upholding thread safety.

Code

Let’s have a look at the code which uses atom:

(def counter (atom 0))
(defn increment-counter []
(swap! counter inc))
(defn reset-counter []
(reset! counter -1))
(println "Initial counter value:" @counter)
(println "Counter after increment:" (increment-counter))
(println "Counter after reset:" (reset-counter))

Explanation

  • Line 1: We create an atom named counter with an initial value of 0.

  • Lines 3–4: We define a function increment-counter that increments the value stored in the counter atom by 1.

  • Lines 6– 7: We define a function reset-counter that sets the value of the counter atom to -1.

  • Line 9: We print the initial value of the counter atom.

  • Line 10: We print the value of the counter atom after incrementing it.

  • Line 11: We print the value of the counter atom after resetting it to -1.

Even though we are using the atom in a simple, single-threaded example where its behavior might look similar to a variable, the atom gives us these additional properties:

  1. Safe state management: We can update it safely without worrying about another thread changing its value unexpectedly.

  2. Controlled mutation: Values inside the atom are immutable, so we can’t accidentally mutate state in uncontrolled ways.

Ref

A ref is a mutable reference type optimized for coordinated, synchronous, and transactional updates. Consistent and atomic state updates are possible when several ref instances are coordinated inside the same transactional context. Updates to ref instances occur within transaction blocks (dosync) using functions like alter, which leverages Clojure’s Software Transactional Memory (STM) mechanism. Refs make sure that all changes made within a transaction are taken back, preserving data integrity, in the event that a transaction fails because it conflicts with other transactions.

Use cases of ref

The following are the typical applications for ref:

  • Coordinated state management: Refs excel in scenarios where multiple pieces of mutable state need to be updated together in a transactional manner to maintain consistency.

  • Concurrent data structures: Refs are suitable for managing concurrent data structures, such as counters or queues, where multiple threads need to coordinate their updates safely.

  • Complex state transformations: Refs are valuable for managing complex state transformations involving multiple steps that must be executed atomically and consistently.

Code

Let’s have a look at the code which uses ref:

(def counter (ref 0))
(defn increment-counter []
(dosync
(alter counter inc)))
(defn reset-counter [value]
(dosync
(ref-set counter value)))
(println "Initial counter value:" @counter)
(println "Counter after increment:" (increment-counter))
(println "Counter after reset:" (reset-counter -1))

Explanation

  1. Line 1: We define a ref named counter with an initial value of 0.

  2. Lines 3–5: We define a function increment-counter that increments the value stored in the counter ref within a transaction. Inside the function body, dosync initiates a transactional block. Transactions ensure that updates to mutable references occur atomically and consistently. Within the transaction, (alter counter inc) is used to modify the value stored in the counter ref.

  3. Lines 7–9: We define a function reset-counter that sets the value of the counter ref to a specified value within a transaction. Inside the function body, dosync starts a transactional block. Within the transaction, ref-set is used to set the value of the counter ref to the specified value.

  4. Line 11–13: We print the initial value, value after incrementing, and value after resetting of the counter ref respectively.

Atom vs. Ref

Here’s a table summarizing the key points of atom and ref in Clojure:

Definition

A mutable reference type for holding a single, immutable value

A mutable reference type for coordinated, transactional updates

Update mechanism

Updated using swap!, reset!, or compare-and-set!

Updated within a transactional block (dosync) using alter or ref-set

Concurrency control

Atomic updates; each atom manages its state independently

Synchronous, transactional updates coordinated across multiple refs

Thread safety

Thread-safe updates to mutable references

Thread-safe and consistent state management within transactions

Mutability

The atom is mutable (the reference can be updated), but the value it holds is immutable

The ref is mutable, and updates to multiple refs can occur atomically in a transaction

Use cases

Managing isolated, independent state, caching mechanisms

Managing coordinated, concurrent state updates, complex state transformations

Example code

clojure (swap! counter inc)

clojure (dosync (alter counter inc))

Conclusion

While both atom and ref provide mechanisms for managing mutable states in Clojure, they differ in their characteristics and use cases. Atoms are suitable for managing independent mutable state, while refs are designed for coordinated, transactional updates across multiple references.

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved