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.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptSTM (Software Transactional Memory)
open Axial
open Axial.State
AxialStateWhile Ref is perfect for updating a single variable, STM is designed for scenarios where you
need to update multiple variables consistently. Axial ensures that the entire transaction is
executed atomically, 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 toRef<'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 anstmblock as a single atomic effect within aflow.
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.
stm {
let! balance = TRef.get account
do! TRef.set (balance - amount) account
return! TRef.get account
}stm {
let! (balance: decimal) =
(TRef.get account: STM<decimal>)
do! (TRef.set (balance - amount) account: STM<unit>)
return! (TRef.get account: STM<decimal>)
}
// 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
}
transferFunds: TRef<decimal> -> TRef<decimal> -> decimal -> STM<unit>fromAcc: TRef<decimal>Axial.State.TRef`1Represents a transactional reference that can be updated atomically within an transaction. The type of the value stored in the reference.
decimalAn abbreviation for the CLI type . Basic Types
toAcc: TRef<decimal>amount: decimalstm: StmBuilderThe stm { } computation expression for building atomic transactions.
currentFrom: decimalAxial.State.TRefModuleget: TRef<'T> -> STM<'T>Reads the current value of the transactional reference within a transaction. The transactional reference to read. An STM operation that produces the current value of the reference. let tx (counter: TRef<int>) = stm { let! value = TRef.get counter return value }
set: 'T -> TRef<'T> -> STM<unit>Sets the value of the transactional reference within a transaction. The new value to store in the reference. The transactional reference to update. An STM operation that sets the reference value. let tx (counter: TRef<int>) = stm { do! TRef.set 10 counter }
(-): ^T1 -> ^T2 -> ^T3Overloaded subtraction operator The first parameter. The second parameter. The result of the operation. 10 - 2 // Evaluates to 8
update: ('T -> 'T) -> TRef<'T> -> STM<unit>Updates the value of the transactional reference within a transaction using the supplied function. The function to apply to the current value to produce the new value. The transactional reference to update. An STM operation that updates the reference value. let tx (counter: TRef<int>) = stm { do! TRef.update (fun n -> n + 1) counter }
balance: decimal(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
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"
}
processTransfer: TRef<decimal> -> TRef<decimal> -> decimal -> Flow<'a,'b,string>fromAcc: TRef<decimal>toAcc: TRef<decimal>amount: decimalflow: FlowBuilderThe universal flow { } computation expression.
Axial.State.STMatomically: STM<'T> -> Flow<'env,'none,'T>Executes an STM transaction atomically within a flow while preserving retry/orElse coordination. The STM transaction to execute. A flow that performs the transaction and returns its result. Axial Flow's STM uses a single mutual-exclusion section around the commit/read step to ensure atomicity and coordinate retries. While this avoids complex optimistic concurrency control, it means that transactions are mutually exclusive and high contention can impact throughput. A transaction that retries does not block the calling thread (or, on Fable, the single JS thread) while it waits: it suspends on a broadcast signal that every successful commit resolves, then re-runs from the start. let transfer (fromAcc: TRef<int>) (toAcc: TRef<int>) amount = stm { let! bal = TRef.get fromAcc if bal < amount then do! STM.retry do! TRef.set (bal - amount) fromAcc do! TRef.update (fun b -> b + amount) toAcc } let flow = STM.atomically (transfer acc1 acc2 100)
transferFunds: TRef<decimal> -> TRef<decimal> -> decimal -> STM<unit>STM transactions are first-class values. You can compose multiple small transactions into a larger one using stm {} before ever calling atomically. This is the "Software" in STM—it allows for modular, composable concurrency.
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
deposit: TRef<decimal> -> decimal -> STM<unit>acc: TRef<decimal>Axial.State.TRef`1Represents a transactional reference that can be updated atomically within an transaction. The type of the value stored in the reference.
decimalAn abbreviation for the CLI type . Basic Types
amount: decimalAxial.State.TRefModuleupdate: ('T -> 'T) -> TRef<'T> -> STM<unit>Updates the value of the transactional reference within a transaction using the supplied function. The function to apply to the current value to produce the new value. The transactional reference to update. An STM operation that updates the reference value. let tx (counter: TRef<int>) = stm { do! TRef.update (fun n -> n + 1) counter }
x: decimal(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
batchTransfer: TRef<decimal> -> TRef<decimal> -> TRef<decimal> -> decimal -> Flow<'a,'b,unit>acc1: TRef<decimal>acc2: TRef<decimal>acc3: TRef<decimal>stm: StmBuilderThe stm { } computation expression for building atomic transactions.
transferFunds: TRef<decimal> -> TRef<decimal> -> decimal -> STM<unit>(/): ^T1 -> ^T2 -> ^T3Overloaded division operator The first parameter. The second parameter. The result of the operation. 16 / 2 // Evaluates to 8
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.State.STMatomically: STM<'T> -> Flow<'env,'none,'T>Executes an STM transaction atomically within a flow while preserving retry/orElse coordination. The STM transaction to execute. A flow that performs the transaction and returns its result. Axial Flow's STM uses a single mutual-exclusion section around the commit/read step to ensure atomicity and coordinate retries. While this avoids complex optimistic concurrency control, it means that transactions are mutually exclusive and high contention can impact throughput. A transaction that retries does not block the calling thread (or, on Fable, the single JS thread) while it waits: it suspends on a broadcast signal that every successful commit resolves, then re-runs from the start. let transfer (fromAcc: TRef<int>) (toAcc: TRef<int>) amount = stm { let! bal = TRef.get fromAcc if bal < amount then do! STM.retry do! TRef.set (bal - amount) fromAcc do! TRef.update (fun b -> b + amount) toAcc } let flow = STM.atomically (transfer acc1 acc2 100)
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
It is important to note that Axial's current STM implementation is based on a global synchronizing 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. - Blocking Retry: When
STM.retryis called, the calling thread is suspended usingMonitor.Waituntil another transaction successfully commits a change. - 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 providing the standard STM programming model found in languages like Haskell or ZIO.
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. |

