Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

STM (Software Transactional Memory)

open Axial
open Axial.State

Software Transactional Memory (STM) is a concurrency primitive that lets you compose multiple atomic operations into a single transaction.

Ref updates one variable atomically. STM updates several variables in one atomic transaction, and supports retry / orElse style coordination for transactions that need to wait on state changes or fall back to alternate branches.

Core Concepts

  • TRef<'T>: A transactional reference. Similar to Ref<'T>, but designed to be used inside an STM transaction.
  • stm { ... }: A computation expression used to compose transactional operations.
  • STM.retry: Aborts the current branch and waits for a committed state change before retrying.
  • STM.orElse: Tries a fallback branch when the first branch retries.
  • STM.atomically: The bridge that executes an stm block as a single atomic effect within a flow.

Basic Usage

Defining a Transaction

Use stm {} to combine reads and writes into one transaction.

let! binds the value produced by an STM step to the name on its left. do! binds an STM step returning unit. return! uses another STM value as the result of the transaction.

let withdraw (amount: decimal) (account: TRef<decimal>) : STM<decimal> =
    stm {
        let! balance = TRef.get account
        do! TRef.set (balance - amount) account
        return! TRef.get account
    }

Here is the same transaction with the left- and right-hand types shown:

let withdrawAnnotated (amount: decimal) (account: TRef<decimal>) : STM<decimal> =
    stm {
        let! (balance: decimal) =
            (TRef.get account: STM<decimal>)

        do! (TRef.set (balance - amount) account: STM<unit>)
        return! (TRef.get account: STM<decimal>)
    }
let transferFunds (fromAcc: TRef<decimal>) (toAcc: TRef<decimal>) (amount: decimal) =
    stm {
        let! currentFrom = TRef.get fromAcc
        
        do! TRef.set (currentFrom - amount) fromAcc
        do! TRef.update (fun balance -> balance + amount) toAcc
    }

Running the Transaction

To execute an stm block, you use STM.atomically. This turns the transaction into a standard Flow.

let processTransfer fromAcc toAcc amount =
    flow {
        // Run the transaction atomically
        do! STM.atomically (transferFunds fromAcc toAcc amount)
        return "Transfer complete"
    }

Composition

STM transactions are first-class values. You can compose multiple small transactions into a larger one using stm {} before ever calling atomically. Transactions built in separate functions combine into one atomic transaction.

let deposit (acc: TRef<decimal>) (amount: decimal) =
    TRef.update (fun x -> x + amount) acc

let batchTransfer acc1 acc2 acc3 amount =
    stm {
        // Composite transaction
        do! transferFunds acc1 acc2 amount
        do! deposit acc3 (amount / 2.0m)
    }
    |> STM.atomically
## Why use STM instead of lock?

Using manual lock calls is error-prone and often leads to deadlocks when different parts of your code acquire multiple locks in inconsistent orders.

Axial's STM keeps commit atomicity inside the engine and adds retry / orElse coordination so a transaction can wait for a relevant state change or fall back to another branch without exposing lock management in user code.

Implementation Details

Axial's current STM implementation runs each transaction under one global lock rather than an optimistic or lock-free model.

  • Atomicity: The entire stm {} block is executed while holding a global lock, ensuring no other transaction can interfere.
  • Suspending retry: When STM.retry is called, the transaction suspends without blocking a thread until another transaction commits, then reruns from the start. Every commit wakes every waiting transaction.
  • Performance: Because of the global lock, transactions are mutually exclusive. This is suitable for coordinating low-frequency state changes but may become a bottleneck under high contention.

This design prioritizes correctness and simplicity for the initial release while keeping the usual STM programming model: transactional references, composable transactions, retry, and orElse.

API Reference: Module TRef

Function Signature Description
make 'T -> STM<TRef<'T>> Creates a new transactional reference.
get TRef<'T> -> STM<'T> Reads the value of a TRef within a transaction.
set 'T -> TRef<'T> -> STM<unit> Sets the value of a TRef within a transaction.
update ('T -> 'T) -> TRef<'T> -> STM<unit> Atomically updates a TRef within a transaction.

API Reference: Module STM

Function Signature Description
retry STM<'T> Requests that the current branch waits and reruns after a committed change.
orElse STM<'T> -> STM<'T> -> STM<'T> Falls back to a second branch when the first branch retries.
atomically STM<'T> -> Flow<'env, 'none, 'T> Executes a composed transaction as an atomic flow.

The STM torture test runs hundreds of interrupted transfers against an auditor and checks that every audit sees a consistent total.