State
Use an atom to advance state in a thread-safe manner:
(def counter (atom 0))
(defn update [step]
(swap! counter #(+ % step))
(deref counter))
(update 3) ; 3
(update 5) ; 8
(deref counter) ; 8
@counter ; 8
The function passed to swap!
shall be referentially transparent (or
“pure”), because it is re-evaluated if the atom’s underlying value
changed between reading and updating it.
Use an agent if the atomic update is accompanied by a side-effect that only shall be executed once:
(def counter (agent 0))
(defn update [step]
(send
counter
(fn [c]
(println "update counter from" c "by" step)
(+ c step)))
(deref counter))
(update 3)
;; update counter from 0 by 3
;; 0
(update 5)
;; update counter from 3 by 5
;; 3
@counter ; 8
The function passed to send
is executed asynchronuously, and the
value is only updated later.
Use ref
to advance the state of multiple items in a single
transaction:
(def consumed (ref []))
(def spent (ref 0.0))
(defn consume [product price]
(dosync
(alter consumed #(conj % product))
(alter spent #(+ % price))))
(consume "beer" 5.25) ; 5.25
(consume "cheese" 7.95) ; 13.2
@consumed ; ["beer" "cheese"]
@spent ; 13.2
How to pick between a var, an atom, an agent, and a ref?
- Use vars for values that do not change.
- Use atoms when advancing state using an update function without any side-effect.
- Use agents when the update function involves side-effects.
- Use refs when updating multiple values together in a transaction.
Before using refs, consider aggregating the values to a map wrapped by an atom.
Exercises
Memoized Fibonacci
Define a var fib-cache
that holds and atom to a map. Write a
function fib-mem
that expects a parameter n
. The function shall
compute the nth Fibonacci number, but cache the results in
fib-cache
.
Hint: Only compute the result if it’s not to be found in the
cache. After calculating the result, update fib-cache
using swap!
.
Test: (fib-mem 60)
shall return 2504730781961
and finish within
reasonable time, i.e. (time (fib-mem 60))
shall finish within the
magnitude of 100 milliseconds.
Function Call Tracker
Expanding the solution from the last exercise, define an additional
var fib-agent
that holds a map to be initialized as {:memoized 0 :recursive 0}
.
Write a function log-call
that expects a parameter kind
(either
:memoized
or :recursive
). The function increments the value to the
respective key by one.
Then extend fib-mem
so that it invokes log-call
whenever the
function is called.
Now write a function fib
that expects a parameter n
and computes
the nth Fibonacci number using plain recursion. Use the same agent to
track the calls to this function, but using the :recursive
key.
Hint: Invoke log-call
as the first thing in fib-mem
. Use the
send
function to call the agent.
Test: After calling (fib-mem 10)
and (fib 10)
, @fib-agent
shall
return {:memoized 19, :recursive 177}
.
Selling Stock in Transactions
Given the following inventory of products:
[{:name "Apple" :quantity 143 :price 0.55}
{:name "Sausage" :quantity 17 :price 3.98}
{:name "Milk" :quantity 198 :price 1.65}
{:name "Coffee" :quantity 23 :price 7.95}
{:name "Bread" :quantity 32 :price 2.45}]
Write a function sell
that accepts two parameters name
and
amount
, and returns the total price of the items sold, or nil
if
there is no such item in the inventory, or if the given amount
surmounts the available quantity.
The function not only calculates the price, but has following side effects:
- The amount is subtracted from the quantity.
- The total price as to be added to the revenue account.
- For every sale,
8.1%
of the selling price has to be added to the VAT account.
Hint: Wrap the inventory, the revenue, and the VAT in a ref
. Update
the refs together using dosync
and alter
.
Test: (sell 15 "Apple")
shall return 8.25
; then @revenue
shall
return 8.25
, @VAT
shall return 0.66825
, and @inventory
shall
return:
[{:name "Apple" :quantity 128 :price 0.55}
;; …
]