References
8.4. Mutable References
While ordinary state monads encode stateful computations with tuples that track the contents of the state along with the computation's value, Lean's runtime system also provides mutable references that are always backed by mutable memory cells.
Mutable references have a type IO.Ref
that indicates that a cell is mutable, and reads and writes must be explicit.
IO.Ref
is implemented using ST.Ref
, so the entire ST.Ref
API may also be used with IO.Ref
.
IO.Ref (α : Type) : Type
8.4.1. State Transformers
Mutable references are often useful in contexts where arbitrary side effects are undesired. They can give a significant speedup when Lean is unable to optimize pure operations into mutation, and some algorithms are more easily expressed using mutable references than with state monads. Additionally, it has a property that other side effects do not have: if all of the mutable references used by a piece of code are created during its execution, and no mutable references from the code escape to other code, then the result of evaluation is deterministic.
The ST
monad is a restricted version of IO
in which mutable state is the only side effect, and mutable references cannot escape.ST
was first described by John Launchbury and Simon L Peyton Jones, 1994. “Lazy functional state threads”. In Proceedings of the ACM SIGPLAN 1994 Conference on Programming Language Design and Implementation..
ST
takes a type parameter that is never used to classify any terms.
The runST
function, which allow escape from ST
, requires that the ST
action that is passed to it can instantiate this type parameter with any type.
This unknown type does not exist except as a parameter to a function, which means that values whose types are “marked” by it cannot escape its scope.
ST (σ : Type) : Type → Type
runST {α : Type} (x : (σ : Type) → ST σ α) : α
As with IO
and EIO
, there is also a variation of ST
that takes a custom error type as a parameter.
Here, ST
is analogous to BaseIO
rather than IO
, because ST
cannot result in errors being thrown.
EST (ε σ : Type) : Type → Type
ST.Ref (σ α : Type) : Type
ST.mkRef {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (a : α) : m (ST.Ref σ α)
8.4.1.1. Reading and Writing
ST.Ref.get {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) : m α
ST.Ref.set {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) (a : α) : m Unit
Data races with get
and set
def main : IO Unit := do
let balance ← IO.mkRef (100 : Int)
let mut orders := #[]
IO.println "Sending out orders..."
for _ in [0:100] do
let o ← IO.asTask (prio := .dedicated) do
let cost ← IO.rand 1 100
IO.sleep (← IO.rand 10 100).toUInt32
if cost < (← balance.get) then
IO.sleep (← IO.rand 10 100).toUInt32
balance.set ((← balance.get) - cost)
orders := orders.push o
-- Wait until all orders are completed
for o in orders do
match o.get with
| .ok () => pure ()
| .error e => throw e
if (← balance.get) < 0 then
IO.eprintln "Final balance is negative!"
else
IO.println "Final balance is zero or positive."
stdout
Sending out orders...
stderr
Final balance is negative!
ST.Ref.modify {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) (f : α → α) : m Unit
Avoiding data races with modify
This program launches 100 threads.
Each thread simulates a purchase attempt: it generates a random price, and if the account balance is sufficient, it decrements it by the price.
The balance check and the computation of the new value occur in an atomic call to ST.Ref.modify
.
def main : IO Unit := do
let balance ← IO.mkRef (100 : Int)
let mut orders := #[]
IO.println "Sending out orders..."
for _ in [0:100] do
let o ← IO.asTask (prio := .dedicated) do
let cost ← IO.rand 1 100
IO.sleep (← IO.rand 10 100).toUInt32
balance.modify fun b =>
if cost < b then
b - cost
else b
orders := orders.push o
-- Wait until all orders are completed
for o in orders do
match o.get with
| .ok () => pure ()
| .error e => throw e
if (← balance.get) < 0 then
IO.eprintln "Final balance negative!"
else
IO.println "Final balance is zero or positive."
stdout
Sending out orders...
Final balance is zero or positive.
stderr
<empty>
ST.Ref.modifyGet {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α β : Type} (r : ST.Ref σ α) (f : α → β × α) : m β
8.4.1.2. Comparisons
ST.Ref.ptrEq {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r1 r2 : ST.Ref σ α) : m Bool
8.4.1.3. Concurrency
Mutable references can be used as a locking mechanism.
Taking the contents of the reference causes attempts to take it or to read from it to block until it is set
again.
This is a low-level feature that can be used to implement other synchronization mechanisms; it's usually better to rely on higher-level abstractions when possible.
ST.Ref.take {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) : m α
ST.Ref.swap {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) (a : α) : m α
ST.Ref.toMonadStateOf {σ : Type} {m : Type → Type} [MonadLiftT (ST σ) m] {α : Type} (r : ST.Ref σ α) : MonadStateOf α m
Reference Cells as Locks
This program launches 100 threads.
Each thread simulates a purchase attempt: it generates a random price, and if the account balance is sufficient, it decrements it by the price.
If the balance is not sufficient, then it is not decremented.
Because each thread take
s the balance cell prior to checking it and only returns it when it is finished, the cell acts as a lock.
Unlike using ST.Ref.modify
, which atomically modifies the contents of the cell using a pure function, other IO
actions may occur in the critical section
This program's main
function is marked Lean.Parser.Command.declaration : command
unsafe
because take
itself is unsafe.
unsafe def main : IO Unit := do
let balance ← IO.mkRef (100 : Int)
let validationUsed ← IO.mkRef false
let mut orders := #[]
IO.println "Sending out orders..."
for _ in [0:100] do
let o ← IO.asTask (prio := .dedicated) do
let cost ← IO.rand 1 100
IO.sleep (← IO.rand 10 100).toUInt32
let b ← balance.take
if cost ≤ b then
balance.set (b - cost)
else
balance.set b
validationUsed.set true
orders := orders.push o
-- Wait until all orders are completed
for o in orders do
match o.get with
| .ok () => pure ()
| .error e => throw e
if (← validationUsed.get) then
IO.println "Validation prevented a negative balance."
if (← balance.get) < 0 then
IO.eprintln "Final balance negative!"
else
IO.println "Final balance is zero or positive."
The program's output is:
stdout
Sending out orders...
Validation prevented a negative balance.
Final balance is zero or positive.