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.JavaScriptGet started
This page has one program in it. Install the package, paste the program into a script, and run it. Everything after that adds one requirement at a time to the same program.
Before you begin
Install the .NET SDK 8.0 or later, then install Axial:
dotnet add package Axial
Axial is the package. Flow<'env, 'error, 'value> is the type it gives you: a description of asynchronous work,
the environment it reads, and the failure it can produce.
Run your first workflow
Put this in a project that references Axial, or save it as checkout.fsx with #r "nuget: Axial" as the first
line and run dotnet fsi checkout.fsx:
open System.Threading
open System.Threading.Tasks
open Axial
type CheckoutError =
| OrderNotFound of orderId: int
| PaymentDeclined of reason: string
type Receipt = { OrderId: int; Total: decimal; Reference: string }
type CheckoutEnv =
{ FindTotal: int -> CancellationToken -> Task<Result<decimal, CheckoutError>>
Charge: decimal -> CancellationToken -> Task<Result<string, CheckoutError>> }
let checkout orderId : Flow<CheckoutEnv, CheckoutError, Receipt> =
flow {
let! findTotal = Flow.envWith _.FindTotal
let! charge = Flow.envWith _.Charge
let! total = ColdTask(fun cancellationToken -> findTotal orderId cancellationToken)
let! reference = ColdTask(fun cancellationToken -> charge total cancellationToken)
return { OrderId = orderId; Total = total; Reference = reference }
}
let live =
{ FindTotal =
fun orderId _ ->
if orderId = 42 then
Task.FromResult(Ok 19.99m)
else
Task.FromResult(Error(OrderNotFound orderId))
Charge = fun _ _ -> Task.FromResult(Ok "ch_1a2b3c") }
let report orderId =
match checkout orderId |> Flow.run live with
| Exit.Success receipt -> printfn $"paid %.2f{receipt.Total} for order {receipt.OrderId} ({receipt.Reference})"
| Exit.Failure cause -> printfn $"{Cause.prettyPrint string cause}"
report 42
report 7
SystemThreadingTasksAxial01-getting-started__index.md_page.CheckoutErrorOrderNotFoundorderId: intintAn abbreviation for the CLI type . Basic Types
PaymentDeclinedreason: stringstringAn abbreviation for the CLI type . Basic Types
01-getting-started__index.md_page.ReceiptOrderId: intTotal: decimaldecimalAn abbreviation for the CLI type . Basic Types
Reference: string01-getting-started__index.md_page.CheckoutEnvFindTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>System.Threading.CancellationTokenPropagates notification that operations should be canceled.
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
Charge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>checkout: int -> Flow<CheckoutEnv,CheckoutError,Receipt>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.
flow: FlowBuilderThe universal flow { } computation expression.
findTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>Axial.FlowenvWith: ('env -> 'value) -> Flow<'env,'error,'value>Projects one value from the current environment. This is the primary way to access app dependencies, configuration, or request metadata stored in env. The projection runs only when the flow is executed, so constructing the flow is still pure and side-effect free. Prefer small projections over passing a large environment deeper into reusable helpers. A function that extracts a value from the environment. A containing the projected value. let currentTime () = Flow.envWith (fun (environment: BaseRuntime) -> environment.Clock.UtcNow())
_arg1: CheckoutEnvcharge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>_arg3: CheckoutEnvtotal: decimalColdTaskcancellationToken: CancellationTokenreference: stringlive: CheckoutEnv(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
report: int -> 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
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
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.
receipt: Receiptprintfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
FailureThe workflow failed due to a specific cause.
cause: Cause<CheckoutError>paid 19.99 for order 42 (ch_1a2b3c)
Fail(OrderNotFound 7)
Three things happened in that program:
Flow.envWithselected a dependency from the environment. The workflow never constructs its dependencies and never looks them up in a container.ColdTaskkept task creation cold, passed the runtime cancellation token to each dependency, and routed eachResult.Errorinto the workflow's expected-error channel.Flow.runsupplied the environment at one boundary and returned anExitthat is either a success value or aCause.
The signature states the whole contract. CheckoutEnv is what the workflow needs, CheckoutError is what callers
must handle, and Receipt is the success value.
Add the requirements that plain Task makes you hand-roll
A real checkout has more rules than the version above: hold a database connection, give up after five seconds, and retry a declined payment a few times but never retry a missing order.
Written against Task, each rule is a separate mechanism, and the signature records none of them:
let checkout (cancellationToken: CancellationToken) (services: AppServices) orderId =
task {
use connection = services.OpenConnection()
use timeoutSource = CancellationTokenSource.CreateLinkedTokenSource cancellationToken
timeoutSource.CancelAfter(TimeSpan.FromSeconds 5.0)
let mutable attempt = 1
let mutable result = Unchecked.defaultof<Result<Receipt, CheckoutError>>
let mutable finished = false
while not finished do
match! chargeOnce connection timeoutSource.Token orderId with
| Error(PaymentDeclined _) when attempt < 3 ->
do! Task.Delay(100 * attempt, timeoutSource.Token)
attempt <- attempt + 1
| outcome ->
result <- outcome
finished <- true
return result
}In Axial the same three rules are three combinators wrapped around the workflow you already wrote:
open System
open System.Threading
open System.Threading.Tasks
open Axial
type Connection = { Name: string }
let openConnection (_: CancellationToken) = Task.FromResult { Name = "orders-db" }
let closeConnection (connection: Connection) (_: CancellationToken) =
task { printfn $"closed {connection.Name}" } :> Task
let retryPayment =
{ RetryPolicy.noDelay 3 with
Delay = fun attempt -> TimeSpan.FromMilliseconds(100.0 * float attempt)
ShouldRetry =
function
| PaymentDeclined _ -> true
| OrderNotFound _ -> false }
let checkoutOrder orderId : Flow<CheckoutEnv, CheckoutError, Receipt> =
flow {
let! _connection =
Flow.acquireReleaseWith (Flow.fromTask openConnection) closeConnection Flow.ok
return! checkout orderId
}
|> Flow.Runtime.retry retryPayment
|> Flow.Runtime.timeout (TimeSpan.FromSeconds 5.0) (PaymentDeclined "checkout timed out")
SystemThreadingTasksAxial01-getting-started__index.md_page.ConnectionName: stringstringAn abbreviation for the CLI type . Basic Types
openConnection: CancellationToken -> Task<Connection>System.Threading.CancellationTokenPropagates notification that operations should be canceled.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
closeConnection: Connection -> CancellationToken -> Taskconnection: Connectiontask: TaskBuilderBuilds a task using computation expression syntax.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
retryPayment: RetryPolicy<CheckoutError>Axial.RetryPolicy`1Defines how runtime retry helpers repeat typed failures in a controlled way.
Axial.RetryPolicyStandard retry policies for runtime helpers.
noDelay: int -> RetryPolicy<'error>Delay: int -> TimeSpanattempt: intSystem.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
float: ^T -> floatConverts the argument to 64-bit float. This is a direct conversion for all primitive numeric types. For strings, the input is converted using Double.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted float float 'A' // evaluates to 65.0 float 0xff // evaluates to 255.0 float -10 // evaluates to -10.0
ShouldRetry: 'error -> boolPaymentDeclinedOrderNotFoundcheckoutOrder: int -> Flow<CheckoutEnv,CheckoutError,Receipt>orderId: intAxial.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.
01-getting-started__index.md_page.CheckoutEnv01-getting-started__index.md_page.CheckoutError01-getting-started__index.md_page.Receiptflow: FlowBuilderThe universal flow { } computation expression.
_connection: ConnectionAxial.FlowacquireReleaseWith: Flow<'env,'error,'resource> -> ('resource -> CancellationToken -> Task) -> ('resource -> Flow<'env,'error,'value>) -> Flow<'env,'error,'value>Acquires a resource, uses it, and always runs the release action. The flow that acquires the resource. The release action to run after the resource is used. The flow that uses the acquired resource. A flow that releases the resource after use, including failure paths. Use this for lexical acquire/use/release. For resources that should live until the surrounding scope closes, use .
fromTask: (CancellationToken -> Task<'value>) -> Flow<'env,'error,'value>Creates a flow from a cancellable task factory. The factory runs on each execution and receives the runtime's cancellation token, so the flow stays cold and cancellable. Thrown exceptions are recorded as defects (Cause.Die). Use attemptTask when expected exceptions should enter the typed error channel. Starts the operation, observing the supplied cancellation token. .NET only let flow = Flow.fromTask (fun token -> client.GetStringAsync(url, token))
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.
checkout: int -> Flow<CheckoutEnv,CheckoutError,Receipt>(|>): '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
retry: RetryPolicy<'error> -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Retries typed failures according to the specified policy. The retry policy. The source flow. A flow that retries Cause.Fail outcomes when the policy allows it. Defects and interruptions are not retried.
Axial.Flow.RuntimeRuntime helpers for execution-time concerns like cancellation, scope, timeout, retry, and cleanup.
timeout: TimeSpan -> 'error -> Flow<'env,'error,'value> -> Flow<'env,'error,'value>Fails with the supplied typed error when the flow does not complete before the timeout. The timeout duration. The typed error returned when the timeout wins. The source flow. A flow that returns the source outcome or the timeout error.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
closed orders-db
Note what the type did not change to. checkoutOrder is still
int -> Flow<CheckoutEnv, CheckoutError, Receipt>, because cancellation, the connection's lifetime, and the retry
loop are the runtime's job rather than the caller's. The timeout produces a CheckoutError value that the caller
already handles instead of an exception the caller has to know about.
Swap the boundary in a test
A test replaces the environment record and leaves the workflow alone:
let declineOnce =
let mutable attempts = 0
{ FindTotal = fun _ _ -> Task.FromResult(Ok 19.99m)
Charge =
fun _ _ ->
attempts <- attempts + 1
if attempts = 1 then
Task.FromResult(Error(PaymentDeclined "insufficient funds"))
else
Task.FromResult(Ok "ch_retry") }
let retried = checkoutOrder 42 |> Flow.run declineOnce
declineOnce: CheckoutEnvattempts: intFindTotal: int -> CancellationToken -> Task<Result<decimal,CheckoutError>>System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
Charge: decimal -> CancellationToken -> Task<Result<string,CheckoutError>>(+): ^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"
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
PaymentDeclinedretried: Exit<Receipt,CheckoutError>checkoutOrder: int -> Flow<CheckoutEnv,CheckoutError,Receipt>(|>): '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
The retry wraps the acquisition, so the second attempt opens and closes its own connection. Move the
Flow.acquireReleaseWith call outside the Flow.Runtime.retry call when every attempt should share one connection.
What's next
- Why Flow? explains when this model earns its cost and when
ResultorTaskis still the right answer. - Installation and packages covers the package map.
- Add Axial to an existing Task application shows the one-module adoption path.
- Your first application runs a Flow as an application root.
- Creating and running flows covers the full construction and execution surface.
- Expected errors and defects explains the error channel and defects.
- Dependencies, services, and layers scales the environment record up.

