Skip to content

Search content

From F# to Clojure > 7: State and Concurrency

From F# to Clojure Part 7
Immutable state snapshots orbit a bright atom with transactional links.

F# makes immutable bindings easy but still exposes general mutable cells:

Table of contents

On this page

Independent State with Atoms

F# can protect a mutable value with lock. A Clojure atom packages the identity and atomic update operation together:

let inventoryGate = obj ()
let mutable inventory = Map [ "apples", 2; "pears", 3 ]

let restockApples amount =
    lock inventoryGate (fun () ->
        let current = inventory |> Map.find "apples"
        inventory <- inventory |> Map.add "apples" (current + amount)
        inventory)

restockApples 4
// map [("apples", 6); ("pears", 3)]F#
(def inventory (atom {:apples 2 :pears 3}))

@inventory
;; => {:apples 2, :pears 3}

(swap! inventory update :apples + 4)
;; => {:apples 6, :pears 3}

@inventory
;; => {:apples 6, :pears 3}Clojure

swap! applies a pure function to the current value and installs the result with a compare-and-set loop. Under contention, Clojure may call that function more than once, so do not send email, write files, or perform other side effects inside it.

Reject Invalid States

F# can place validation in a setter function, but every caller must use that function. An atom validator applies to every proposed value regardless of the update path:

let mutable balance = 100

let setBalance value =
    if value < 0 then
        invalidArg "value" "Balance cannot be negative"

    balance <- value
    balance

setBalance 70
// 70F#
(def balance
  (atom 100 :validator #(>= % 0)))

(swap! balance - 30)
;; => 70Clojure

An update that would produce a negative balance is rejected before it becomes visible. A validator protects the state boundary regardless of which function attempts the update.

Coordinated State with Refs

F# can serialize a transfer with one shared lock. Clojure refs compose the two updates inside a software transactional memory transaction without exposing a lock:

let transferGate = obj ()
let checking = ref 100
let savings = ref 50

let transfer (fromAccount: int ref) (toAccount: int ref) amount =
    lock transferGate (fun () ->
        if amount <= 0 then
            invalidArg "amount" "Transfer must be positive"

        if fromAccount.Value < amount then
            invalidOp "Insufficient funds"

        fromAccount.Value <- fromAccount.Value - amount
        toAccount.Value <- toAccount.Value + amount
        fromAccount.Value, toAccount.Value)

transfer checking savings 30
// (70, 80)F#
(def checking (ref 100))
(def savings (ref 50))

(defn transfer! [from to amount]
  (dosync
    (when-not (pos? amount)
      (throw (ex-info "Transfer must be positive" {:amount amount})))
    (when (< @from amount)
      (throw (ex-info "Insufficient funds" {:amount amount})))
    (alter from - amount)
    (alter to + amount)
    [@from @to]))

(transfer! checking savings 30)
;; => [70 80]Clojure

Other threads see either both updates or neither. A transaction may retry, so keep I/O and other irreversible effects outside dosync too.

Pick by Coordination Model

ReferenceUse when
AtomOne identity changes synchronously
RefSeveral identities must change transactionally
AgentIndependent updates can run asynchronously
VolatileOne thread needs a lightweight mutable cell

Most functions should still accept and return ordinary values. Put a reference at the edge that owns change rather than passing mutable identities throughout the program.


Share this post

Previous in series
From F# to Clojure > 6: From Discriminated Unions to Open Polymorphism
Next in series
From F# to Clojure > 8: From Pipelines to Transducers