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

Ref (Atomic References)

Ref<'T> holds a mutable value that concurrent workflows can update atomically.

Verified
open Axial
open Axial.State

Why Use Ref?

In a functional program, state is usually passed as parameters or through the environment. Some state, such as a shared counter, a cache, or a status flag, needs several concurrent workflows to observe and update the same value.

Ref uses lightweight per-instance locking to make updates atomic without manual lock management.

Creating a Ref

Use Ref.make to create a new reference with an initial value. Creating a Ref returns a Flow.

Verified
> open Axial.State;;
> let counted () : Flow<int> =
-     flow {
-         let! counter = Ref.make 0
-         do! Ref.update ((+) 1) counter
-         return! Ref.get counter
-     };;
> Flow.run () (counted ());;
> Flow.run () (counted ());;
val it: Exit<int,Never> = Success 1
val it: Exit<int,Never> = Success 1

Each run makes a fresh Ref, so running counted twice gives 1 both times.

Reading and Writing

Use Ref.get to read the current value and Ref.set to overwrite it.

Verified
let workWithRef (counter: Ref<int>) =
    flow {
        let! value = Ref.get counter
        do! Ref.set (value + 1) counter
        return value
    }
Requires & types
Requires: open Axial; open Axial.State (packages: Axial)
val workWithRef: Ref<int> -> Flow<'a,'b,int>

Atomic Updates

Use Ref.update or Ref.modify to update a value atomically when multiple fibers share it.

Ref.update

Updates the value using a transformation function.

Verified
let increment (counter: Ref<int>) =
    Ref.update (fun x -> x + 1) counter
Requires & types
Requires: open Axial.State (packages: Axial)
val increment: Ref<int> -> Flow<'a,'b,unit>

Ref.modify

Updates the value and returns a derived result (a common pattern for "get and increment").

Verified
let incrementAndGet (counter: Ref<int>) =
    Ref.modify (fun x -> 
        let next = x + 1
        next, next // (return value, new state)
    ) counter
Requires & types
Requires: open Axial.State (packages: Axial)
val incrementAndGet: Ref<int> -> Flow<'a,'b,int>

The atomic family

Ref.getAndSet, Ref.getAndUpdate, and Ref.updateAndGet cover the common "get the previous/updated value while changing state" cases without writing a custom modify function:

Verified
let incrementAndGetWithHelper (counter: Ref<int>) =
    Ref.updateAndGet (fun x -> x + 1) counter

let snapshotAndReset (counter: Ref<int>) =
    Ref.getAndSet 0 counter
Requires & types
Requires: open Axial.State (packages: Axial)
val incrementAndGetWithHelper: Ref<int> -> Flow<'a,'b,int>
val snapshotAndReset: Ref<int> -> Flow<'a,'b,int>

Example: Shared Progress Tracker

Verified
let trackProgress (total: int) =
    flow {
        let! progress = Ref.make 0
        
        let doStep = Ref.updateAndGet (fun p -> p + 1) progress
            
        // Run several steps
        for _ in 1 .. total do
            do! doStep |> Flow.ignore
            
        return "Complete"
    }
Requires & types
Requires: open Axial; open Axial.State (packages: Axial)
val trackProgress: int -> Flow<'a,'b,string>

API Reference

The full Ref signature list — make, get, set, update, modify, and the getAndSet / getAndUpdate / updateAndGet family — is in the Ref API reference.

The refs torture test runs every update operation from eight fibers at once and checks that no update is lost and no two see the same value.