Atoms
Prerequisites: Higher-order functions
Values never change, but programs have state that does: a counter, a cache, the current users. Clojure models state as a reference that points to an immutable value and can be switched, safely, to point to a new one. The simplest reference type is the atom.
(def counter (atom 0))
(println @counter) ; deref: read the current value
(swap! counter inc) ; apply a function to it
(swap! counter + 10)
(println @counter)
(reset! counter 100) ; set a new value directly
(println (deref counter))
0
11
100
@atom(or(deref atom)) reads the current value.(swap! atom f args...)sets the value to(f current args...)and returns the new value.reset!replaces the value without looking at the old one.
Atoms holding maps
(def app-state (atom {:users {} :next-id 1}))
(defn add-user! [name]
(swap! app-state
(fn [{:keys [next-id] :as state}]
(-> state
(assoc-in [:users next-id] {:id next-id :name name})
(update :next-id inc)))))
(add-user! "Ada")
(add-user! "Linus")
(println (:users @app-state))
(println (:next-id @app-state))
{1 {:id 1, :name Ada}, 2 {:id 2, :name Linus}}
3
The typical Clojure application keeps a few atoms of plain data and
updates them with pure functions. The ! suffix marks functions that
change state.
Thread safety
(def hits (atom 0))
(let [workers (doall (for [_ (range 8)]
(future (dotimes [_ 1000] (swap! hits inc)))))]
(run! deref workers))
(println @hits)
(shutdown-agents)
8000
Eight threads (created with future — see Futures, Promises, and
Delays) incremented the atom 8,000 times without a lock, and no
update was lost. swap! works by compare-and-set: if another thread
changed the atom in the meantime, it retries with the new value. That's
why the function you pass to swap! must be pure — it may run more
than once.
Watches and validators
(def temperature (atom 20 :validator #(<= -50 % 60)))
(add-watch temperature :logger
(fn [_key _ref old new]
(println "temperature" old "->" new)))
(swap! temperature + 5)
(reset! temperature 18)
(try
(reset! temperature 500)
(catch IllegalStateException e
(println "rejected:" (ex-message e))))
(println @temperature)
temperature 20 -> 25
temperature 25 -> 18
rejected: Invalid reference state
18
- A validator rejects invalid values (the atom keeps the old one).
- A watch runs after every change — useful for logging, UI updates, or keeping a derived value in sync.
Practice
Build a tiny shopping cart with an atom holding a map of item to
quantity, and functions add-item! and remove-item!.
Show solution
(def cart (atom {}))
(defn add-item! [item]
(swap! cart update item (fnil inc 0)))
(defn remove-item! [item]
(swap! cart (fn [c]
(if (> (get c item 0) 1)
(update c item dec)
(dissoc c item)))))
(add-item! :apple)
(add-item! :apple)
(add-item! :bread)
(remove-item! :bread)
(remove-item! :apple)
(println @cart)
{:apple 1}