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.JavaScriptScopes
Axial's flow { } supports F# use and use! for .NET resources:
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 readFirstLine path =
flow {
use reader = File.OpenText path
return! ColdTask(fun _ -> reader.ReadLineAsync())
}
readFirstLine: string -> Flow<'a,'b,string>path: stringflow: FlowBuilderThe universal flow { } computation expression.
reader: StreamReaderSystem.IO.FileProvides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of objects.
OpenText: string -> StreamReaderOpens an existing UTF-8 encoded text file for reading. The file to be opened for reading. A on the specified path. The caller does not have the required permission. is a zero-length string, contains only white space, or contains one or more invalid characters as defined by . is . The specified path, file name, or both exceed the system-defined maximum length. The specified path is invalid, (for example, it is on an unmapped drive). The file specified in was not found. is in an invalid format.
ColdTaskReadLineAsync: unit -> Task<string>Reads a line of characters asynchronously from the current stream and returns the data as a string. A task that represents the asynchronous read operation. The value of the parameter contains the next line from the stream, or is if all the characters have been read. The number of characters in the next line is larger than . The stream has been disposed. The reader is currently in use by a previous read operation.
This is a lexical lifetime. The compiler disposes reader when control leaves the body governed by that use. In a
flow { }, disposal happens before the Flow produced by that body completes. Another function or later workflow cannot
safely use the resource.
File.OpenText and ReadLineAsync are ambient .NET filesystem calls, not the IFileSystem service, so this example is
about lifetime only: a missing file or failed read throws instead of producing a typed error. For the recommended
pattern — an explicit filesystem dependency with a typed error channel — see
FileSystem.
A Flow scope is a runtime ownership boundary. It lets resources and child fibers live across function and subflow calls, then closes all of them together after success, failure, interruption, or defect.
Scoped resources
A scoped resource is anything whose cleanup is registered with a Flow scope instead of a lexical use:
- a value acquired with
Flow.scopeResourceorFlow.scopeAcquireRelease - cleanup registered with
Flow.scopeFinalizer,Flow.scopeDisposable, orFlow.scopeAsyncDisposable - what Axial registers for you: a hub subscription, a queue from
Queue.makeScoped, and a fiber fromFlow.fork, which the scope interrupts if it is still running
A scoped resource is released when the scope it was registered with closes. This page is about which scope that is:
the current one, a child created with Flow.scoped, or, in concurrent code, the scope of a branch or fiber (see
Concurrency and scoped resources).
Lifetime maps
The diagrams use containment literally: Flow B is a subflow inside Flow A, and a resource bar sits inside the boundary that owns it.
Lexical use ends with its subflow body
Flow B owns reader lexically. When Flow B returns to Flow A, reader has already been disposed.
Registration in the current scope outlives Flow B
Flow B ends, but R does not. Flow.scopeResource and the other scope... functions register with the current scope,
so R remains alive while Flow A continues and is cleaned only when that scope closes.
Flow.scoped creates an earlier cleanup boundary
Flow B ends before R does; the child scope closes before Flow A ends. Flow.scoped sets a cleanup boundary
independently of the function or subflow that acquired the resource.
Create a local runtime scope
/// A stand-in connection that records when it is opened and closed.
let lifecycle = ResizeArray<string>()
type Connection(name: string) =
member _.Name = name
let openConnection : Flow<string, Connection> =
Flow.delay (fun () ->
lifecycle.Add "open"
Flow.ok (Connection "orders"))
let closeConnection (connection: Connection) : Flow<unit> =
Flow.delay (fun () ->
lifecycle.Add $"close {connection.Name}"
Flow.ok ())
let runApplicationWork (connection: Connection) : Flow<string, string> =
Flow.delay (fun () ->
lifecycle.Add "work"
Flow.ok $"used {connection.Name}")
let application : Flow<string, string> =
Flow.scoped (
flow {
let! connection =
Flow.scopeAcquireRelease
openConnection
closeConnection
return! runApplicationWork connection
})
lifecycle: ResizeArray<string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
stringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage3_10B692608C6D.Connectionname: string_: ConnectionName: Connection -> unit -> stringopenConnection: Flow<string,Connection>FlowA flow that requires no environment and can fail with a typed error.
Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Add: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
``.ctor``: string -> ConnectioncloseConnection: Connection -> Flow<unit>connection: ConnectionFlowA flow that requires no environment and cannot fail with a typed error.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
runApplicationWork: Connection -> Flow<string,string>application: Flow<string,string>scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
flow: FlowBuilderThe universal flow { } computation expression.
scopeAcquireRelease: Flow<'env,'error,'resource> -> ('resource -> Flow<'env,Never,unit>) -> Flow<'env,'error,'resource>Acquires a value and registers its release with the current runtime scope. Same as Resource.create acquire release |> Flow.scopeResource; see Resource.create for how the release runs. The flow that acquires the value. The release flow run when the current scope closes. A flow that succeeds with the acquired value.
lifecycle.Clear()
application |> Flow.run () |> shouldEqual (Exit.Success "used orders")
List.ofSeq lifecycle |> shouldEqual [ "open"; "work"; "close orders" ]
lifecycle: ResizeArray<string>Clear: unit -> unitRemoves all elements from the .
application: Flow<string,string>(|>): '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
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.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
Resources registered inside Flow.scoped remain available across calls inside that block. Cleanup finishes
before the resulting Flow returns.
How a release runs
A release is itself a flow, 'value -> Flow<'env, Never, unit>, so it can update a Ref, call a service from the
environment, or wait on asynchronous work. When the scope closes:
- The release runs with the environment the value was acquired with, even if the scope closes somewhere else.
- It runs without interruption. Closing a scope because its flow was interrupted does not cancel the releases.
- Its error type is
Never, so it cannot fail with a typed error. A release that throws or dies becomes a defect in theExitof the flow that owned the scope. All releases still run — they run in reverse registration order, and a failing release does not skip the others — and their failures are collected and folded into thatExit. If the scope closed because its flow had already failed, the release defect is appended to that failure's cause; if the flow had succeeded, the release defect alone turns the result into anExit.Failure.
let releaseOrder = ResizeArray<string>()
let releaseA : Flow<unit> =
Flow.delay (fun () -> releaseOrder.Add "release A"; Flow.ok ())
let releaseC : Flow<unit> =
Flow.delay (fun () -> releaseOrder.Add "release C"; raise (InvalidOperationException "boom"))
let releaseB : Flow<unit> =
Flow.delay (fun () -> releaseOrder.Add "release B"; Flow.ok ())
let failingBody : Flow<string, unit> =
Flow.scoped (
flow {
do! Flow.scopeFinalizer releaseA
do! Flow.scopeFinalizer releaseC
do! Flow.scopeFinalizer releaseB
return! Flow.fail "body failed"
})
releaseOrder: ResizeArray<string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty and has the default initial capacity.
stringAn abbreviation for the CLI type . Basic Types
releaseA: Flow<unit>FlowA flow that requires no environment and cannot fail with a typed error.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.Flowdelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Add: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
releaseC: Flow<unit>raise: Exception -> 'TRaises an exception The exception to raise. The result value. open System.IO exception FileNotFoundException of string let readFile (fileName: string) = if not (File.Exists(fileName)) then raise(FileNotFoundException(fileName)) File.ReadAllText(fileName) readFile "/this-file-doest-exist" When executed, raises a FileNotFoundException.
``.ctor``: string -> unitInitializes a new instance of the class with a specified error message. The message that describes the error.
releaseB: Flow<unit>failingBody: Flow<string,unit>FlowA flow that requires no environment and can fail with a typed error.
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
flow: FlowBuilderThe universal flow { } computation expression.
scopeFinalizer: Flow<'env,Never,unit> -> Flow<'env,'error,unit>Registers a release flow with the current runtime scope. Use this when cleanup should run when the surrounding runtime, fiber, or layer scope closes, rather than when the current expression ends (Flow.ensuring). The release runs without interruption and with the current environment; a defect it raises becomes a defect of the flow that closes the scope. The release flow run when the current scope closes. A flow that registers the release.
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")
releaseOrder.Clear()
match failingBody |> Flow.run () with
| Exit.Failure cause ->
Cause.failures cause |> shouldEqual [ "body failed" ]
Cause.defects cause |> List.map (fun error -> error.Message) |> shouldEqual [ "boom" ]
List.ofSeq releaseOrder |> shouldEqual [ "release B"; "release C"; "release A" ]
| other -> failwithf "Expected a failure, got %A" other
releaseOrder: ResizeArray<string>Clear: unit -> unitRemoves all elements from the .
failingBody: Flow<string,unit>(|>): '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
Axial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
FailureThe workflow failed due to a specific cause.
cause: Cause<string>Axial.Causefailures: Cause<'error> -> 'error listReturns every typed failure value contained in a cause tree.
shouldEqual: 'a -> 'a -> unitdefects: Cause<'error> -> exn listReturns every defect exception contained in a cause tree.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
error: exnMessage: stringGets a message that describes the current exception. The error message that explains the reason for the exception, or an empty string ("").
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
other: Exit<unit,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.
All three releases ran, in reverse registration order, even though release C threw; the typed failure from the
body and the release defect both appear in the final Exit.
Register with the current scope
The scope prefix means “attach this value or cleanup action to the current Flow scope”:
let registerEverything (stream: IDisposable) (response: IAsyncDisposable) : Flow<string, unit> =
flow {
do! Flow.scopeDisposable stream
do! Flow.scopeAsyncDisposable response
do! Flow.scopeFinalizer (Flow.delay (fun () -> lifecycle.Add "flush telemetry"; Flow.ok ()))
do! Flow.scopeFinalizer (Flow.fromAsync (async { lifecycle.Add "save state" }))
}
registerEverything: IDisposable -> IAsyncDisposable -> Flow<string,unit>stream: IDisposableSystem.IDisposableProvides a mechanism for releasing unmanaged resources.
response: IAsyncDisposableSystem.IAsyncDisposableProvides a mechanism for releasing unmanaged resources asynchronously.
FlowA flow that requires no environment and can fail with a typed error.
stringAn abbreviation for the CLI type . Basic Types
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
Axial.FlowscopeDisposable: IDisposable -> Flow<'env,'error,unit>Registers a disposable resource with the current runtime scope. The disposable resource to close when the current scope closes. A flow that registers the resource.
scopeAsyncDisposable: IAsyncDisposable -> Flow<'env,'error,unit>Registers an asynchronously disposable resource with the current runtime scope. The async disposable resource to close when the current scope closes. A flow that registers the resource.
scopeFinalizer: Flow<'env,Never,unit> -> Flow<'env,'error,unit>Registers a release flow with the current runtime scope. Use this when cleanup should run when the surrounding runtime, fiber, or layer scope closes, rather than when the current expression ends (Flow.ensuring). The release runs without interruption and with the current environment; a defect it raises becomes a defect of the flow that closes the scope. The release flow run when the current scope closes. A flow that registers the release.
delay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
lifecycle: ResizeArray<string>Add: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
fromAsync: Async<'value> -> Flow<'env,'error,'value>Creates a flow from a raw async operation. Thrown exceptions are recorded as defects (Cause.Die). Cancellation is recorded as interruption when the runtime token requested it, and as a defect otherwise. Use attemptAsync when expected exceptions should enter the typed error channel. Fable compatible
async: AsyncBuilderBuilds an asynchronous workflow using computation expression syntax. let sleepExample() = async { printfn "sleeping" do! Async.Sleep 10 printfn "waking up" return 6 } sleepExample() |> Async.RunSynchronously
Finalizers run in the reverse of their registration order when the scope closes:
lifecycle.Clear()
let stream = { new IDisposable with member _.Dispose() = lifecycle.Add "dispose stream" }
let response = { new IAsyncDisposable with member _.DisposeAsync() = lifecycle.Add "dispose response"; ValueTask() }
registerEverything stream response |> Flow.scoped |> Flow.run () |> shouldEqual (Exit.Success())
List.ofSeq lifecycle |> shouldEqual [ "save state"; "flush telemetry"; "dispose response"; "dispose stream" ]
lifecycle: ResizeArray<string>Clear: unit -> unitRemoves all elements from the .
stream: IDisposableSystem.IDisposableProvides a mechanism for releasing unmanaged resources.
_: IDisposableDispose: unit -> unitPerforms application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
Add: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
response: IAsyncDisposableSystem.IAsyncDisposableProvides a mechanism for releasing unmanaged resources asynchronously.
_: IAsyncDisposableDisposeAsync: unit -> ValueTaskPerforms application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously. A task that represents the asynchronous dispose operation.
``.ctor``: unitregisterEverything: IDisposable -> IAsyncDisposable -> Flow<string,unit>(|>): '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.Flowscoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
run: '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
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.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
Use Flow.scopeDisposable and Flow.scopeAsyncDisposable for resources that already exist. Use
Flow.scopeFinalizer for custom cleanup.
Flow.scopeAcquireRelease combines acquisition and registration without an interruption point between them:
type RequestCache() =
member _.Entries = Collections.Generic.Dictionary<string, string>()
interface IDisposable with
member _.Dispose() = lifecycle.Add "cache disposed"
let acquireRequestCache : Flow<string, RequestCache> =
Flow.scopeAcquireRelease
(Flow.succeed (new RequestCache()))
(fun cache ->
Flow.delay (fun () ->
(cache :> IDisposable).Dispose()
Flow.ok ()))
FsLiveDocsGeneratedPage3_10B692608C6D.RequestCache_: RequestCacheEntries: RequestCache -> unit -> Collections.Generic.Dictionary<string,string>Collections``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
GenericstringAn abbreviation for the CLI type . Basic Types
System.IDisposableProvides a mechanism for releasing unmanaged resources.
Dispose: RequestCache -> unit -> unitlifecycle: ResizeArray<string>Add: string -> unitAdds an object to the end of the . The object to be added to the end of the . The value can be for reference types.
acquireRequestCache: Flow<string,RequestCache>FlowA flow that requires no environment and can fail with a typed error.
Axial.FlowscopeAcquireRelease: Flow<'env,'error,'resource> -> ('resource -> Flow<'env,Never,unit>) -> Flow<'env,'error,'resource>Acquires a value and registers its release with the current runtime scope. Same as Resource.create acquire release |> Flow.scopeResource; see Resource.create for how the release runs. The flow that acquires the value. The release flow run when the current scope closes. A flow that succeeds with the acquired value.
succeed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
cache: RequestCachedelay: (unit -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Defers flow construction until execution time. A function that returns the flow to execute. Recursive calls deferred here are consumed by the executor without growing a chain of parent tasks. Work that must run after the recursive call still needs storage until that call finishes. A flow that lazily evaluates the factory when executed. let flow = Flow.delay (fun () -> Flow.succeed 42)
Dispose: unit -> unitPerforms application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
ok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
The returned value remains available to later subflows in the same scope.
Reusable resource descriptions
Resource separates a reusable acquisition description from the scope that eventually owns it:
let connectionResource = Resource.create openConnection closeConnection
let query (connection: Connection) : Flow<string, int> = Flow.ok connection.Name.Length
let program : Flow<string, int> =
Flow.scoped (
flow {
let! connection =
connectionResource
|> Flow.scopeResource
return! query connection
})
connectionResource: Resource<unit,string,Connection>Axial.ResourceModulecreate: Flow<'env,'error,'value> -> ('value -> Flow<'env,Never,unit>) -> Resource<'env,'error,'value>Describes acquisition together with a release flow registered in the current Flow scope. The release runs when the scope closes, without interruption and with the environment captured at acquisition. It cannot fail with a typed error; a defect it raises becomes a defect of the flow that closes the scope, after the scope's other releases have run. Resource.create openConnection (fun connection -> Flow.delay (fun () -> connection.Close(); Flow.succeed ()))
openConnection: Flow<string,Connection>closeConnection: Connection -> Flow<unit>query: Connection -> Flow<string,int>connection: ConnectionFsLiveDocsGeneratedPage3_10B692608C6D.ConnectionFlowA flow that requires no environment and can fail with a typed error.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
Axial.Flowok: 'value -> Flow<'env,'error,'value>Creates a successful synchronous flow. The value to wrap in a successful flow. A flow that always succeeds with the provided value.
Length: intGets the number of characters in the current object. The number of characters in the current string.
Name: stringprogram: Flow<string,int>scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
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
scopeResource: Resource<'env,'error,'value> -> Flow<'env,'error,'value>Acquires a described resource and registers its release with the current runtime scope. The resource is a scoped resource: it stays open until the current scope closes. Inside a forked fiber that is the fiber's own scope, which closes when the fiber settles; inside a branch of zipPar, race, sequencePar, or traversePar, the resource moves to the caller's scope if the caller receives the branch's value. Use forkResource to acquire it in the background and keep it. A flow that finishes acquiring after its scope has closed, such as a race loser, releases the resource at once and is interrupted. The acquisition and release description. A flow that succeeds with the acquired value.
lifecycle.Clear()
program |> Flow.run () |> shouldEqual (Exit.Success 6)
List.ofSeq lifecycle |> shouldEqual [ "open"; "close orders" ]
lifecycle: ResizeArray<string>Clear: unit -> unitRemoves all elements from the .
program: Flow<string,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
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.
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
ofSeq: 'T seq -> 'T listBuilds a new list from the given enumerable object. The input sequence. The list of elements from the sequence. let inputs = seq { 1; 2; 5 } inputs |> List.ofSeq Evaluates to [ 1; 2; 5 ]. This is an O(n) operation, where n is the length of the sequence.
Use Resource.finalizer release for cleanup that has no acquired value.
Concurrency and scoped resources
Concurrent flows run in scopes of their own: the flows of one Flow.zipPar, Flow.sequencePar, or parallel
traversal share a scope, each branch of Flow.race has one, and so does each fiber from Flow.fork.
- When you receive a concurrent flow's value, its scoped resources move to your scope as if you had acquired them: they stay open for your later steps and are released when your scope closes.
- When you don't receive the value, its scoped resources are released.
The per-combinator details — what zipPar/sequencePar/traversePar/forEachPar, race, timeout*, fork, and
forkResource each do with their scoped resources — are in the
Scope API reference.
Flow.fork returns a handle instead of a value. The fiber cannot know whether anyone will join it, so it releases
its scoped resources when it settles. This prevents a finished consumer from holding a hub subscription open and
stalling the publisher.
Acquire in parallel
Acquire several scoped resources at once with Flow.zipPar or Flow.sequencePar. Both stay open after the
combinator returns:
/// Opens a connection, recording the open and the close in `log`.
let connectionTo (log: Ref<string list>) (name: string) : Resource<ClockEnvironment, string, string> =
Resource.create
(log |> Ref.update (fun entries -> entries @ [ $"open {name}" ]) |> Flow.map (fun () -> name))
(fun _ -> log |> Ref.update (fun entries -> entries @ [ $"close {name}" ]))
let closedIn (log: Ref<string list>) : Flow<ClockEnvironment, string, int> =
log |> Ref.get |> Flow.map (List.filter (fun (entry: string) -> entry.StartsWith "close") >> List.length)
/// Opens two connections in parallel and counts the closed ones while they are in use and after their scope closes.
let openBoth : Flow<ClockEnvironment, string, int * int> =
flow {
let! log = Ref.make []
let! whileInUse =
flow {
let! _orders, _customers =
Flow.zipPar (Flow.scopeResource (connectionTo log "orders")) (Flow.scopeResource (connectionTo log "customers"))
return! closedIn log
}
|> Flow.scoped
let! afterScope = closedIn log
return whileInUse, afterScope
}
connectionTo: Ref<string list> -> string -> Resource<ClockEnvironment,string,string>log: Ref<string list>Axial.State.Ref`1Represents a handle to a mutable reference that can be updated atomically. The type of the value stored in the reference.
stringAn 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.
name: stringAxial.Resource`3Describes acquisition of a value together with registration of its release in the current Flow scope. The release is a flow that cannot fail with a typed error. It runs without interruption, with the environment the value was acquired with; a defect it raises becomes a defect of the flow that closes the scope. The environment required to acquire the value. The typed acquisition failure. The acquired value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.ResourceModulecreate: Flow<'env,'error,'value> -> ('value -> Flow<'env,Never,unit>) -> Resource<'env,'error,'value>Describes acquisition together with a release flow registered in the current Flow scope. The release runs when the scope closes, without interruption and with the environment captured at acquisition. It cannot fail with a typed error; a defect it raises becomes a defect of the flow that closes the scope, after the scope's other releases have run. Resource.create openConnection (fun connection -> Flow.delay (fun () -> connection.Close(); Flow.succeed ()))
(|>): '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
entries: string list(@): 'T list -> 'T list -> 'T listConcatenate two lists. The first list. The second list. The concatenation of the lists. let l1 = ['a'; 'b'; 'c'] let l2 = ['d'; 'e'; 'f'] l1 @ l2 // Evaluates to ['a'; 'b'; 'c'; 'd'; 'e'; 'f'] This is an O(n) operation, where n is the length of the first list.
Axial.Flowmap: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
closedIn: Ref<string list> -> Flow<ClockEnvironment,string,int>Axial.Flow`3intAn abbreviation for the CLI type . Basic Types
get: 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 }
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
filter: ('T -> bool) -> 'T list -> 'T listReturns a new collection containing only the elements of the collection for which the given predicate returns "true" The function to test the input elements. The input list. A list containing only the elements that satisfy the predicate. let input = [1, "Luke"; 2, "Kirk"; 3, "Kenobi"; 4, "Spock"] let isEven x = 0 = x % 2 let isComingFromStarTrek (x,_) = isEven x input |> List.filter isComingFromStarTrek Evaluates to [(2, "Kirk"); (4, "Spock")] This is an O(n) operation, where n is the length of the list.
entry: stringStartsWith: string -> boolDetermines whether the beginning of this string instance matches the specified string. The string to compare. if matches the beginning of this string; otherwise, . is .
(>>): ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3Compose two functions, the function on the left being applied first The first function to apply. The second function to apply. The composition of the input functions. let addOne x = x + 1 let doubleIt x = x * 2 let addThenDouble = addOne >> doubleIt addThenDouble 3 // Evaluates to 8
length: 'T list -> intReturns the length of the list. The input list. The length of the list. The notation list.Length is preferred. This is an O(n) operation, where n is the length of the list. let inputs = [ "a"; "b"; "c" ] inputs |> List.length Evaluates to 3 This is an O(n) operation, where n is the length of the list.
openBoth: Flow<ClockEnvironment,string,(int * int)>flow: FlowBuilderThe universal flow { } computation expression.
make: '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(())
whileInUse: int_orders: string_customers: stringzipPar: Flow<'env,'error,'left> -> Flow<'env,'error,'right> -> Flow<'env,'error,('left * 'right)>Combines two flows into a tuple of their values, running them concurrently. If either flow fails, the other is interrupted immediately. The two flows share a scope of their own. When both succeed, the scoped resources they acquired move to the caller's scope, as if the caller had acquired them, so zipPar (scopeResource a) (scopeResource b) acquires two resources in parallel and both stay open until the caller's scope closes. When zipPar fails, both flows' scoped resources are released before it returns. The first flow to combine. The second flow to combine. A flow that returns a tuple of both successful values. let combined = Flow.zipPar (Flow.succeed 1) (Flow.succeed 2) combined |> Flow.run ()
scopeResource: Resource<'env,'error,'value> -> Flow<'env,'error,'value>Acquires a described resource and registers its release with the current runtime scope. The resource is a scoped resource: it stays open until the current scope closes. Inside a forked fiber that is the fiber's own scope, which closes when the fiber settles; inside a branch of zipPar, race, sequencePar, or traversePar, the resource moves to the caller's scope if the caller receives the branch's value. Use forkResource to acquire it in the background and keep it. A flow that finishes acquiring after its scope has closed, such as a race loser, releases the resource at once and is interrupted. The acquisition and release description. A flow that succeeds with the acquired value.
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
afterScope: intopenBoth |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success(0, 2))
openBoth: Flow<ClockEnvironment,string,(int * 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.
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.
Both connections remain open while openBoth uses them and close with its scope.
Acquire in the background and keep it
To start acquiring one resource, do other work, and use the resource later, use Flow.forkResource. The acquisition
runs in a fiber, but the release belongs to the caller's scope, so Fiber.join returns a live resource. A plain
Flow.fork of the same acquisition returns a resource that the fiber has already released:
/// A plain fork: the fiber owns the connection and releases it when it settles, before join returns it.
let forkedConnectionClosedOnJoin : Flow<ClockEnvironment, string, bool> =
flow {
let! log = Ref.make []
let! fiber = Flow.scopeResource (connectionTo log "audit") |> Flow.fork
let! _ = Fiber.join fiber
let! entries = Ref.get log
return List.contains "close audit" entries
}
|> Flow.scoped
/// forkResource: the caller's scope owns the connection, so it is still open after join.
let keptConnection : Flow<ClockEnvironment, string, bool * string list> =
flow {
let! log = Ref.make []
let! closedOnJoin =
flow {
let! connecting = Flow.forkResource (connectionTo log "audit")
let! _ = Fiber.join connecting
let! entries = Ref.get log
return List.contains "close audit" entries
}
|> Flow.scoped
let! entries = Ref.get log
return closedOnJoin, entries
}
forkedConnectionClosedOnJoin: Flow<ClockEnvironment,string,bool>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
boolAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
log: Ref<string list>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(())
fiber: Fiber<string,string>Axial.FlowscopeResource: Resource<'env,'error,'value> -> Flow<'env,'error,'value>Acquires a described resource and registers its release with the current runtime scope. The resource is a scoped resource: it stays open until the current scope closes. Inside a forked fiber that is the fiber's own scope, which closes when the fiber settles; inside a branch of zipPar, race, sequencePar, or traversePar, the resource moves to the caller's scope if the caller receives the branch's value. Use forkResource to acquire it in the background and keep it. A flow that finishes acquiring after its scope has closed, such as a race loser, releases the resource at once and is interrupted. The acquisition and release description. A flow that succeeds with the acquired value.
connectionTo: Ref<string list> -> string -> Resource<ClockEnvironment,string,string>(|>): '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
fork: 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.
Axial.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 }
entries: string listget: 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 }
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
contains: 'T -> 'T list -> boolTests if the list contains the specified element. The value to locate in the input list. The input list. True if the input list contains the specified element; false otherwise. [1..9] |> List.contains 0 Evaluates to false. [1..9] |> List.contains 3 Evaluates to true. let input = [1, "SpongeBob"; 2, "Patrick"; 3, "Squidward"; 4, "Mr. Krabs"] input |> List.contains (2, "Patrick") Evaluates to true. let input = [1, "SpongeBob"; 2, "Patrick"; 3, "Squidward"; 4, "Mr. Krabs"] input |> List.contains (22, "Patrick") Evaluates to false. This is an O(n) operation, where n is the length of the list.
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
keptConnection: Flow<ClockEnvironment,string,(bool * string list)>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.
closedOnJoin: boolconnecting: Fiber<string,string>forkResource: Resource<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts acquiring a resource in a new fiber, keeping the resource in the caller's scope. A fiber started with fork releases its scoped resources when it settles, so a resource it acquires is already released by the time Fiber.join returns it. forkResource runs only the acquisition in the fiber and registers the release with the caller's scope, so Fiber.join returns a live resource that is released when the caller's scope closes, as if the caller had used scopeResource. If the caller's scope closes before the acquisition finishes, the fiber is interrupted, and a resource that was acquired anyway is released straight away. Scoped resources that the acquiring flow itself registers along the way belong to the fiber and are released when it settles; only the described resource moves to the caller. To acquire several resources in parallel and use them together, use zipPar or sequencePar. The acquisition and release description. A flow that produces a fiber whose value is the acquired resource. flow { let! connecting = Flow.forkResource databaseConnection let! config = loadConfig let! connection = Fiber.join connecting // still open return! migrate connection config } |> Flow.scoped // the connection is released here
forkedConnectionClosedOnJoin |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success true)
keptConnection |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success(false, [ "open audit"; "close audit" ]))
forkedConnectionClosedOnJoin: Flow<ClockEnvironment,string,bool>(|>): '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.
keptConnection: Flow<ClockEnvironment,string,(bool * string list)>If the caller's scope closes before the acquisition finishes, the fiber is interrupted, and a resource acquired anyway
is released straight away. Axial.Guardrails
reports a forked flow that returns its own scoped resource as AXG007.
Coming from ZIO or Effect
In ZIO 2 and Effect, a forked fiber shares its parent's Scope, so forking an acquireRelease and joining the fiber
returns a live resource. In Axial a forked fiber owns its scope, like ZIO.scoped(effect).fork; write
Flow.forkResource for that pattern.
zipPar, race, and the parallel traversals behave as in ZIO and Effect for values you receive. What a race loser or
a failed branch acquired is released as soon as Axial discards that flow, not when the scope closes. If the flow
stored the resource somewhere else first, such as in a Ref, that reference points at a released resource.
Nested scopes
A fiber created with Flow.fork is itself registered with the current scope. Closing the scope interrupts and awaits an
unfinished fiber, then releases what the fiber acquired. A nested Flow.scoped gives resources and fibers a lifetime shorter than the surrounding application without passing
cleanup handles through every function.
Child scopes are also owned by their parent. If the root execution is interrupted, it closes every remaining child in reverse registration order.
Streams
FlowStream terminal consumers create child scopes internally. The lifecycle of that scope is:
- Building a
FlowStreamopens nothing and creates no scope. - Running the terminal Flow creates the stream's child scope.
- Pulling starts the source. Enumerators, adapter handles, and parallel mapping fibers register with that scope.
- Operators and their subflows use the same stream scope unless they explicitly call
Flow.scoped. - The terminal consumer stops pulling after completion, failure, interruption, or an operator such as
FlowStream.takeending early. - It closes the child scope, runs finalizers in reverse registration order, interrupts and awaits unfinished child
fibers, and only then returns its
Exit.
An enumerator opened by FlowStream.fromSeq is disposed before FlowStream.runCollect returns. Likewise, when
FlowStream.mapFlowPar has outstanding mappings and downstream stops after FlowStream.take, the terminal scope
interrupts and awaits those mappings before the terminal Flow completes. The
consuming streams guide explains the practical cleanup guarantee from the stream user's
perspective.
Choosing a scope
- One lexical body exclusively owns a disposable →
useoruse!. - Ownership spans functions, subflows, fibers, or stream pulls →
Flow.scoped. - Acquire in the background and keep the resource →
Flow.forkResource; several in parallel →Flow.zipParorFlow.sequencePar. - Everything else stays on the current scope:
scope...functions attach to it and do not clean up immediately, finalizers run in reverse registration order at most once, and cleanup failures are defects that combine with the failure that caused closure. The lifetime maps and concurrency section above give the full detail.
Layers use the same model. Layer.fromFlow runs a flow in the layer's scope, so what it acquires with
Flow.scopeAcquireRelease stays alive until Layer.provide finishes; Layer.acquireRelease does the same for a
resource built by another layer.

