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.JavaScriptDeferred and Semaphore
Axial includes a small set of concurrency primitives only where they add Axial semantics over the .NET primitives underneath.
Use .NET Task, Channel<T>, SemaphoreSlim, and ConcurrentQueue<T> directly when raw platform behavior is enough. Use Axial primitives when coordination should preserve typed Exit and Cause, participate in workflow interruption, or release resources through the Flow model.
Deferred
Deferred<'error, 'value> is a one-shot handoff point between fibers. It can be completed once with a full Exit<'value, 'error>, so success, typed failure, defects, and interruption all remain visible to waiters.
Completion operations are idempotent. They return true to the caller that completed the deferred value and false to later callers.
Shared setup
// Setup for the checked examples on this page.
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
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
let handoff : Flow<ClockEnvironment, string, int> =
flow {
let! deferred = Deferred.make<ClockEnvironment, string, int> ()
let! waiter =
Deferred.await deferred
|> Flow.fork
let! completed = Deferred.succeed 42 deferred
let! value = Fiber.join waiter
if completed then
return value
else
return! Flow.fail "deferred was already completed"
}
handoff: Flow<ClockEnvironment,string,int>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
deferred: Deferred<string,int>Axial.DeferredModuleFlow-native helpers for one-shot typed coordination.
make: unit -> Flow<'env,'error,Deferred<'error,'value>>Creates an empty deferred value.
waiter: Fiber<string,int>await: Deferred<'error,'value> -> Flow<'env,'error,'value>Waits for the deferred outcome, preserving success, typed failure, defect, or interruption.
(|>): '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.Flowfork: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber without waiting for it to complete. Forking turns a cold flow description into hot child work and returns a handle that can later be joined or interrupted. Prefer zipPar or race when the caller only needs a simple parallel composition. Wait for the handle with Fiber.join or Fiber.await, and stop it with Fiber.interrupt. The flow to fork. A flow that produces a handle.
completed: boolsucceed: 'value -> Deferred<'error,'value> -> Flow<'env,'workflowError,bool>Attempts to complete the deferred value successfully.
value: intAxial.FiberModuleOperations on a running , the handle returned by Flow.fork. Every operation returns a flow; nothing waits or interrupts until that flow runs. Reading a fiber's outcome through join, await, or interrupt marks it observed, so a defect it died with is not also reported as unobserved.
join: Fiber<'error,'value> -> Flow<'env,'error,'value>Waits for a fiber and returns its value, failing the same way the fiber failed. Joining preserves the child's error channel: a Cause.Fail becomes the same typed error, and interruption and defects remain interruption and defects. Use await to inspect the outcome instead. The fiber to join. A flow that completes with the fiber's value. let loadProfile : Flow<unit, string, string> = Flow.ok "profile" let loadOrders : Flow<unit, string, int list> = Flow.ok [ 1; 2 ] let page = flow { let! fiber = Flow.fork loadProfile let! orders = loadOrders let! profile = Fiber.join fiber return profile, orders }
fail: 'error -> Flow<'env,'error,'value>Same as error. The error value to wrap in a failing flow. A flow that always fails with the provided error. let result = Flow.fail "error" |> Flow.run () // result = Failure (Cause.Fail "error")
Use Deferred when a fiber needs to wait for a typed outcome produced elsewhere:
Deferred.awaitwaits for the outcome and resumes with the same success or failure.Deferred.completecompletes with a fullExit.Deferred.succeed,Deferred.fail,Deferred.die, andDeferred.interruptcomplete common outcomes directly.
For a synchronous host callback, Deferred.completeNow returns bool immediately, without starting a Flow. The
succeedNow, failNow, dieNow, and interruptNow forms build the corresponding Exit for you. Only one caller
receives true; the callback that receives false did not replace the outcome already stored. The waiting fiber
still uses Deferred.await.
Awaiting respects runtime cancellation. If the waiting workflow is interrupted before the deferred value is completed, the await returns Cause.Interrupt.
Semaphore
FlowSemaphore limits how many workflows can enter a section at the same time. The public API is intentionally scoped: use Semaphore.withPermit instead of raw acquire/release.
let active = ref 0
let busiest = ref 0
let runRequest (request: int) : Flow<ClockEnvironment, string, int> =
flow {
let now = Interlocked.Increment &active.contents
lock busiest (fun () -> busiest.Value <- max busiest.Value now)
do! Flow.sleep (TimeSpan.FromMilliseconds 5.0)
Interlocked.Decrement &active.contents |> ignore
return request * 10
}
let limitedFetch (semaphore: FlowSemaphore) (request: int) : Flow<ClockEnvironment, string, int> =
Semaphore.withPermit semaphore (
flow {
// Only one workflow per permit can run this section.
return! runRequest request
})
active: int refref: 'T -> 'T refCreate a mutable reference cell The value to contain in the cell. The created reference cell. let count = ref 0 // Creates a reference cell object with a mutable Value property count.Value // Evaluates to 0 count.Value <- 1 // Updates the value count.Value // Evaluates to 1
busiest: int refrunRequest: int -> Flow<ClockEnvironment,string,int>request: intintAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
now: intSystem.Threading.InterlockedProvides atomic operations for variables that are shared by multiple threads.
Increment: byref<int> -> intIncrements a specified variable and stores the result, as an atomic operation. The variable whose value is to be incremented. The incremented value. The address of is a null pointer.
(~&): 'T -> byref<'T>Address-of. Uses of this value may result in the generation of unverifiable code. The input object. The managed pointer.
contents: 'TThe current value of the reference cell
lock: 'Lock -> (unit -> 'T) -> 'TExecute the function as a mutual-exclusion region using the input value as a lock. The object to be locked. The action to perform during the lock. The resulting value. open System.Linq /// A counter object, supporting unlocked and locked increment type TestCounter () = let mutable count = 0 /// Increment the counter, unlocked member this.IncrementWithoutLock() = count <- count + 1 /// Increment the counter, locked member this.IncrementWithLock() = lock this (fun () -> count <- count + 1) /// Get the count member this.Count = count let counter = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter.IncrementWithoutLock()) // Evaluates to a number between 1-100000, non-deterministically because there is no locking counter.Count let counter2 = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter2.IncrementWithLock()) // Evaluates to 100000 deterministically because the increment to the counter object is locked counter2.Count
Value: intThe current value of the reference cell
max: 'T -> 'T -> 'TMaximum based on generic comparison The first value. The second value. The maximum value. max 1 2 // Evaluates to 2 max [1;2;3] [1;2;4] // Evaluates to [1;2;4] max "zoo" "alpha" // Evaluates to "zoo"
Axial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
Decrement: byref<int> -> intDecrements a specified variable and stores the result, as an atomic operation. The variable whose value is to be decremented. The decremented value. The address of is a null pointer.
(|>): '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
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
limitedFetch: FlowSemaphore -> int -> Flow<ClockEnvironment,string,int>semaphore: FlowSemaphoreAxial.FlowSemaphoreA Flow-native semaphore handle used to limit concurrent workflow sections.
Axial.SemaphoreModuleFlow-native semaphore helpers.
withPermit: FlowSemaphore -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a workflow while holding one permit and always releases the permit afterward.
Semaphore.withPermit releases the permit after success, typed failure, defect, or interruption. This is the important difference from manually calling WaitAsync and Release: permit cleanup follows the workflow outcome.
Create semaphores with a positive permit count:
let program : Flow<ClockEnvironment, string, int list> =
flow {
let! semaphore = Semaphore.make 2
return! [ 1..8 ] |> Flow.traversePar (Parallelism.bounded 8) (limitedFetch semaphore)
}
program: Flow<ClockEnvironment,string,int list>Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
flow: FlowBuilderThe universal flow { } computation expression.
semaphore: FlowSemaphoreAxial.SemaphoreModuleFlow-native semaphore helpers.
make: int -> Flow<'env,'error,FlowSemaphore>Creates a semaphore with the supplied initial permit count.
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
(|>): '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.FlowtraversePar: Parallelism -> ('value -> Flow<'env,'error,'next>) -> 'value seq -> Flow<'env,'error,'next list>Maps values to flows and runs them with bounded concurrency, returning results in input order. At most mappings run at once; as each finishes, its worker starts the next value. The first failure interrupts the mappings still running and waits for their cleanup before the flow fails, so no sibling is left running in the background. Size CPU-bound work with Parallelism.ofProcessors. When each worker needs its own connection or handle, use traverseParUsing. The maximum number of mappings running at once. Maps each value to a flow. The values to map. A flow containing the mapped values in the order of . let! pages = urls |> Flow.traversePar (Parallelism.bounded 8) fetchPage
Axial.ParallelismModuleCreates bounds for parallel Flow and stream operators.
bounded: int -> ParallelismCreates a positive concurrency bound. Thrown when is not positive.
limitedFetch: FlowSemaphore -> int -> Flow<ClockEnvironment,string,int>program |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success [ 10; 20; 30; 40; 50; 60; 70; 80 ])
busiest.Value <= 2 |> shouldEqual true
program: Flow<ClockEnvironment,string,int list>(|>): '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.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
busiest: int refValue: intThe current value of the reference cell
(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
Eight workers ran, but never more than the two permits at once.
Zero permits are rejected because Axial does not expose an external raw release operation. A semaphore created with zero permits would be a permanently blocked handle rather than a useful concurrency limit.
Queues
To hand a stream of values between fibers, use Queue. It adds bounded, dropping, and sliding strategies, a shutdown that lets the consumer drain its backlog, and interruption that never loses or duplicates a value.
The semaphore and deferred torture tests interrupt waiters and holders at random and check that no permit is lost and exactly one completion wins.

