APISTM

STM

Software transactional memory: shared variables that tasks read and update in composable atomic transactions.

A transaction is a callback typed () ->{STM} a. atomically runs it so that its reads observe one consistent snapshot and its writes become visible to other tasks all at once, or not at all. Several variables therefore change together, and functions performing STM combine into larger transactions. retry abandons an attempt until a variable it read changes, so any condition over shared state becomes a blocking wait. orElse chooses between alternatives that may retry, and attempt undoes the writes of an action that fails.

A transaction may be run more than once: when another task commits to a variable it read, it is discarded and started again. Its row is closed, so it cannot perform IO, Async, or any other effect handled outside it, and replaying it is unobservable. Effects handled inside the transaction, such as a local Fail.attempt or Runtime.Local.run, are allowed. Read outside services before atomically and pass the values in; return side effects that depend on the outcome as data and perform them after it commits. atomically itself needs Async, so transactions do not nest.

Only atomically handles STM; its operations are private to this module.

import Async exposing (Outcome(..))
import STM

result = Async.runOutcome {
    alice = STM.atomically { STM.new 100 }
    bob = STM.atomically { STM.new 0 }
    STM.atomically { STM.modify alice { n -> n - 30 }; STM.modify bob { n -> n + 30 } }
    STM.atomically { (STM.read alice, STM.read bob) }
    }
result == Completed (70, 30)

Only atomically handles STM, so no other module can add a primitive, such as a commit hook, or interpret STM differently; tests run under the real atomically. Everything else is built from the operations here: the bundled STM.MVar and STM.Queue are ordinary modules over them. Granularity is the main choice in such a structure. A Var (Dict k v) makes every update conflict with every other, while a Dict k (Var v) conflicts only per key but needs a fixed key set or a variable for membership.

A handler whose clauses perform STM interprets another effect inside a transaction. Its writes are logged, so they roll back and replay with the transaction, and code written against State runs atomically against shared state:

import Async exposing (Outcome(..))
import State exposing (State)
import STM exposing (STM)

runVar : STM.Var s -> (() ->{State s, e} a) ->{STM, e} a
runVar var action =
    handle action() on
        State.get () -> resume (STM.read var)
        State.put next -> resume (STM.write var next)
        State.update step ->
            (result, next) = step (STM.read var)
            STM.write var next
            resume result

result = Async.runOutcome {
    counter = STM.atomically { STM.new 1 }
    STM.atomically { runVar counter { State.modify { n -> n * 5 } } }
    STM.atomically { STM.read counter }
    }
result == Completed 5

Var

type Var aSource

A shared mutable variable holding an a. Its identity, not its value, distinguishes it; it may cross task boundaries freely. A variable lives until it is unreachable, and has no equality or Show.

STM

effect STMSource

Running inside a transaction. Only atomically handles it.

atomically

atomically : (() ->{STM} a) ->{Async} aSource

Runs transaction until it commits and answers its value.

Every attempt observes one consistent snapshot of the variables it reads; a read that would see a later commit restarts the attempt at once, so a transaction never computes on inconsistent values. Its writes become visible together when it commits. If another task committed to a variable it read in the meantime, the attempt is discarded and the transaction runs again. Conflict restarts are invisible to the program.

A transaction responds to cancellation only while waiting after retry. One that never retries runs to commit even in a cancelled task, so cancellation cleanup can still release a lock or return a permit with atomically.

import Async exposing (Outcome(..))
import STM

Async.runOutcome { STM.atomically { STM.read (STM.new 7) } } == Completed 7

attempt

attempt : (() ->{Fail a, STM} b) ->{STM} Result a bSource

Runs action, answering Ok with its value, or Err with its failure after discarding every write it made. Its reads are kept, so they still decide whether the transaction commits. The transaction itself commits either way; Fail.fromResult raises the error after atomically.

import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))
import STM

result = Async.runOutcome {
    account = STM.atomically { STM.new 10 }
    withdrawn = STM.atomically {
        STM.attempt {
            STM.modify account { n -> n - 30 }
            if STM.read account < 0 then Fail.fail "overdrawn" else ()
        }
    }
    (withdrawn, STM.atomically { STM.read account })
    }
result == Completed (Err "overdrawn", 10)

check

check : Bool ->{STM} ()Source

Does nothing when condition holds, and retrys otherwise.

import Async exposing (Outcome(..))
import STM

result = Async.runOutcome {
    permits = STM.atomically { STM.new 0 }
    worker = Async.spawn {
        STM.atomically {
            available = STM.read permits
            STM.check (available > 0)
            STM.write permits (available - 1)
        }
        "acquired"
    }
    STM.atomically { STM.modify permits { n -> n + 1 } }
    Async.await worker
    }
result == Completed "acquired"

exchange

exchange : Var a -> a ->{STM} aSource

Replaces the variable’s value with next and answers the value it replaced.

import Async exposing (Outcome(..))
import STM

result = Async.runOutcome { STM.atomically { v = STM.new "old"; previous = STM.exchange v "new"; (previous, STM.read v) } }
result == Completed ("old", "new")

modify

modify : Var a -> (a -> a) ->{STM} ()Source

Replaces the variable’s value with f applied to it.

import Async exposing (Outcome(..))
import STM

Async.runOutcome { STM.atomically { v = STM.new 3; STM.modify v { n -> n * 7 }; STM.read v } } == Completed 21

new

new : a ->{STM} Var aSource

A fresh variable holding value. Outside a transaction, use atomically { STM.new value }. A variable created by an attempt that is discarded is never seen by anyone.

import Async exposing (Outcome(..))
import STM

Async.runOutcome { STM.atomically { STM.read (STM.new "hi") } } == Completed "hi"

orElse

orElse : (() ->{STM} a) -> (() ->{STM} a) ->{STM} aSource

Runs first, or second if first retries. The writes first made are discarded before second runs, but its reads are kept: if both retry, the transaction waits until a variable either of them read changes. A conflict in first restarts the whole transaction rather than choosing second.

import Async exposing (Outcome(..))
import STM

result = Async.runOutcome {
    empty = STM.atomically { STM.new 0 }
    full = STM.atomically { STM.new 5 }
    takeFrom = { var ->
        n = STM.read var
        STM.write var 0
        STM.check (n > 0)
        n
    }
    STM.atomically {
        taken = STM.orElse { takeFrom empty } { takeFrom full }
        (taken, STM.read empty, STM.read full)
    }
    }
result == Completed (5, 0, 0)

read

read : Var a ->{STM} aSource

The variable’s value, including the transaction’s own earlier writes.

import Async exposing (Outcome(..))
import STM

Async.runOutcome { STM.atomically { v = STM.new 1; STM.write v 2; STM.read v } } == Completed 2

retry

retry : () ->{STM} aSource

Abandons the attempt: its writes are discarded, and the task waits until another transaction commits a write to a variable the attempt read, then runs the transaction again from the start. A retry that read nothing waits until the task is cancelled.

import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))
import STM

result = Async.runOutcome {
    box = STM.atomically { STM.new Nothing }
    consumer = Async.spawn {
        STM.atomically {
            case STM.read box of
                Just value -> value
                Nothing -> STM.retry ()
        }
    }
    STM.atomically { STM.write box (Just 42) }
    Async.await consumer
    }
result == Completed 42

write

write : Var a -> a ->{STM} ()Source

Replaces the variable’s value when the transaction commits. Later reads in the same transaction see it at once.

import Async exposing (Outcome(..))
import STM

result = Async.runOutcome {
    v = STM.atomically { STM.new [1] }
    STM.atomically { STM.write v [2, 3] }
    STM.atomically { STM.read v }
    }
result == Completed [2, 3]