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
| Reference | Use when |
|---|---|
| Atom | One identity changes synchronously |
| Ref | Several identities must change transactionally |
| Agent | Independent updates can run asynchronously |
| Volatile | One 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.