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.

Solution
(def fib-cache (atom {}))

(defn fib-mem [n]
  (cond
    (< n 2) 1
    (contains? @fib-cache n) (get @fib-cache n)
    :else (let [result (+ (fib-mem (- n 2)) (fib-mem (- n 1)))]
            (swap! fib-cache #(assoc % n result))
            result)))

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}.

Solution
(def fib-agent (agent {:memoized 0 :recursive 0}))

(defn log-call [kind]
  (when (contains? @fib-agent kind)
    (send fib-agent #(assoc % kind (inc (get % kind))))))

(def fib-cache (atom {}))

(defn fib-mem [n]
  (log-call :memoized)
  (cond
    (< n 2) 1
    (contains? @fib-cache n) (get @fib-cache n)
    :else (let [result (+ (fib-mem (- n 2)) (fib-mem (- n 1)))]
            (swap! fib-cache #(assoc % n result))
            result)))

(defn fib [n]
  (log-call :recursive)
  (if (< n 2)
    1
    (+ (fib (- n 2)) (fib (- n 1)))))

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:

  1. The amount is subtracted from the quantity.
  2. The total price as to be added to the revenue account.
  3. 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}
;; …
]
Solution
(defn sell [name amount]
  (let [this-pred #(= (:name %) name)
        item (first (filter this-pred @inventory))]
    (if (and item (>= (:quantity item) amount))
      (let [total (* amount (:price item))
            vat (* total 0.081)
            new-item (assoc item :quantity (- (:quantity item) amount))
            rest-pred (complement this-pred)]
        (dosync
         (alter inventory #(conj (filter rest-pred %) new-item))
         (alter revenue #(+ % total))
         (alter VAT #(+ % vat)))
        total)
      nil)))