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.
Show page 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 actuallet 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`3Axial.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 fiber runs in its own scope and releases its scoped resources (anything registered with a scope, such as scopeResource, scopeFinalizer, or a hub subscription) when it settles, whether or not anyone joins it. A value the fiber returns therefore outlives the scoped resources the fiber acquired: a resource returned through Fiber.join has already been released. Use forkResource to acquire a resource in the background and keep it, or zipPar and sequencePar to acquire several in parallel. 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.
/// How many requests are running now, and the most that ever ran at once.
type Load = { Active: int; Busiest: int }
let runRequest (load: Ref<Load>) (request: int) : Flow<ClockEnvironment, string, int> =
flow {
do! load |> Ref.update (fun l -> { Active = l.Active + 1; Busiest = max l.Busiest (l.Active + 1) })
do! Flow.sleep (TimeSpan.FromMilliseconds 5.0)
do! load |> Ref.update (fun l -> { l with Active = l.Active - 1 })
return request * 10
}
let limitedFetch (semaphore: FlowSemaphore) (load: Ref<Load>) (request: int) : Flow<ClockEnvironment, string, int> =
Semaphore.withPermit semaphore (
flow {
// Only one workflow per permit can run this section.
return! runRequest load request
})
FsLiveDocsGeneratedPage12_F9357565CFEE.LoadActive: intintAn abbreviation for the CLI type . Basic Types
Busiest: intrunRequest: Ref<Load> -> int -> Flow<ClockEnvironment,string,int>load: Ref<Load>Axial.State.Ref`1Represents a handle to a mutable reference that can be updated atomically. The type of the value stored in the reference.
request: intAxial.Flow`3Axial.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.
(|>): '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.RefModuleupdate: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,unit>Updates the value of the reference using the supplied function. The update function of type 'T -> 'T. The to update. A flow that updates the value and returns unit. Ref.update (fun x -> x + 1) myRef
l: Load(+): ^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"
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 .
(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
limitedFetch: FlowSemaphore -> Ref<Load> -> 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 * int> =
flow {
let! semaphore = Semaphore.make 2
let! load = Ref.make { Active = 0; Busiest = 0 }
let! results = [ 1..8 ] |> Flow.traversePar (Parallelism.bounded 8) (limitedFetch semaphore load)
let! final = Ref.get load
return results, final.Busiest
}
program: Flow<ClockEnvironment,string,(int list * int)>Axial.Flow`3Axial.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.
load: Ref<Load>Axial.State.RefModulemake: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
Active: intBusiest: intresults: int list(..): ^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. Scoped resources follow sequencePar: when every mapping succeeds, what each one acquired moves to the caller's scope; when any fails, all of it is released before the traversal returns. 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 -> Ref<Load> -> int -> Flow<ClockEnvironment,string,int>final: Loadget: Ref<'T> -> Flow<'env,'none,'T>Reads the current value of the reference. The to read from. A flow that returns the current value. let readInitialValue () = flow { let! reference = Ref.make 10 return! Ref.get reference }
match program |> Flow.run (ClockEnvironment Clock.live) with
| Exit.Success(results, busiest) ->
results |> shouldEqual [ 10; 20; 30; 40; 50; 60; 70; 80 ]
busiest <= 2 |> shouldEqual true
| other -> failwithf "Expected success, got %A" other
program: Flow<ClockEnvironment,string,(int list * int)>(|>): '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.
Axial.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.
results: int listbusiest: intshouldEqual: 'a -> 'a -> unit(<=): '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
other: Exit<(int list * int),string>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.
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.

