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.JavaScriptAsync and Task interop
Axial distinguishes a description of asynchronous work from work that has already started. Use that distinction to keep
workflows rerunnable, pass cancellation to external operations, and preserve Result.Error in Flow's typed error
channel.
Choose an interop form
| Source | Use | Behavior |
|---|---|---|
Async<'value> |
Bind directly in flow { } |
Cold and rerunnable; the value is successful output |
Async<Result<'value,'error>> |
Bind directly in flow { } |
Cold and rerunnable; Error enters the typed error channel |
ColdTask<'value> |
Bind directly in flow { } |
Cold task factory; receives Flow's cancellation token |
ColdTask<Result<'value,'error>> |
Bind directly in flow { } |
Cold task factory; Error enters the typed error channel |
CancellationToken -> Task<'value> |
Flow.fromTask |
Creates a cold Flow whose task value is successful output |
CancellationToken -> Task<Result<'value,'error>> |
Flow.fromTaskResult |
Creates a cold Flow whose Error enters the typed error channel |
Already-running Task<'value> |
Flow.awaitStartedTask |
Awaits the existing operation; Flow cannot pass cancellation into it |
Already-running Task<Result<'value,'error>> |
Flow.awaitStartedTaskResult |
Awaits the existing operation and lifts Error |
ValueTask has the corresponding fromValueTask, fromValueTaskResult, awaitStartedValueTask, and
awaitStartedValueTaskResult functions.
Bind Async values
Async is already a cold F# computation. Bind it directly:
let loadCount : Async<int> =
async { return 42 }
let workflow =
flow {
let! count = loadCount
return count + 1
}let loadUser : Async<Result<User, LoadUserError>> =
repository.loadUser userId
let workflow : Flow<unit, LoadUserError, string> =
flow {
let! user = loadUser
return user.Name
}let workflow : Flow<unit, LoadUserError, User> =
flow {
return! loadUser
}Bind cold Task work
A Task starts when the method that returns it runs. Wrap the factory in ColdTask so the method runs only when the
Flow runs:
let loadUser : ColdTask<Result<User, LoadUserError>> =
ColdTask(fun cancellationToken ->
repository.loadUserAsync(userId, cancellationToken))
let workflow : Flow<unit, LoadUserError, string> =
flow {
let! user = loadUser
return user.Name
}let workflow : Flow<unit, LoadUserError, User> =
flow {
return! loadUser
}Convert a Task factory without a builder
Use Flow.fromTask when the task returns an ordinary value:
let download : Flow<unit, Never, byte array> =
Flow.fromTask(fun cancellationToken ->
client.GetByteArrayAsync(uri, cancellationToken))let loadUser : Flow<unit, LoadUserError, User> =
Flow.fromTaskResult(fun cancellationToken ->
repository.loadUserAsync(userId, cancellationToken))Await work that already started
Sometimes an API gives you a Task that is already running:
let runningTask = repository.beginRefresh()
let refresh : Flow<unit, Never, RefreshSummary> =
Flow.awaitStartedTask runningTasklet runningTask : Task<Result<RefreshSummary, RefreshError>> =
repository.beginRefresh()
let refresh : Flow<unit, RefreshError, RefreshSummary> =
Flow.awaitStartedTaskResult runningTask- It started before the Flow.
- Reusing the Flow awaits the same operation.
- Flow cannot inject its cancellation token into work that already started.
- Prefer a cold factory when you control task creation.
Raw Task and ValueTask values do not bind directly in flow { }. This prevents an already-running operation from
looking like a cold workflow description.
Handle expected exceptions
The from*, ColdTask, and awaitStarted* paths treat thrown exceptions as defects. Use an attempt* function when
an exception is an expected failure that callers should handle:
let read : Flow<unit, exn, string> =
Flow.attemptTask(fun cancellationToken ->
File.ReadAllTextAsync(path, cancellationToken))Flow.attemptAsync
Flow.attemptTask
Flow.attemptValueTask
Flow.attemptStartedTask
Flow.attemptStartedValueTask
Axial.FlowattemptAsync: Async<'value> -> Flow<'env,exn,'value>Creates a flow from an async operation and treats thrown exceptions as recoverable typed errors. Successful completion returns Exit.Success. OperationCanceledException returns Cause.Interrupt. Other exceptions return Cause.Fail exn. Fable compatible
attemptTask: (CancellationToken -> Task<'value>) -> Flow<'env,exn,'value>Creates a flow from a cancellable task factory and treats thrown exceptions as recoverable typed errors. Successful completion returns Exit.Success. OperationCanceledException returns Cause.Interrupt. Other exceptions return Cause.Fail exn. Starts the operation, observing the supplied cancellation token. .NET only
attemptValueTask: (CancellationToken -> ValueTask<'value>) -> Flow<'env,exn,'value>Creates a flow from a cancellable value-task factory and treats thrown exceptions as recoverable typed errors. Successful completion returns Exit.Success. OperationCanceledException returns Cause.Interrupt. Other exceptions return Cause.Fail exn. Starts the operation, observing the supplied cancellation token. .NET only
attemptStartedTask: Task<'value> -> Flow<'env,exn,'value>Observes a task that has already been started and treats thrown exceptions as recoverable typed errors. Carries the same caveats as awaitStartedTask. A task that is already running. .NET only
attemptStartedValueTask: ValueTask<'value> -> Flow<'env,exn,'value>Observes a value task that has already been started and treats thrown exceptions as recoverable typed errors. Carries the same caveats as awaitStartedTask. A value task that is already running. .NET only
Keep a Result as the successful value
The builder interprets one outer Result as Flow's error channel. Add another successful layer when a nested Result is
the value you intentionally need:
let inspect : ColdTask<Result<Result<User, LoadUserError>, Never>> =
ColdTask(fun cancellationToken ->
task {
let! result = repository.loadUserAsync(userId, cancellationToken)
return (Ok result : Result<_, Never>)
})
let workflow : Flow<unit, Never, Result<User, LoadUserError>> =
flow {
return! inspect
}Summary
- Bind
AsyncandColdTaskdirectly inflow { }. - An outer
Result.Erroralways enters Flow's typed error channel. - Use
ColdTaskorFlow.fromTask*to start task work when the Flow runs. - Use
Flow.awaitStarted*only for work that has already started. - Raw
TaskandValueTaskvalues are not Flow builder sources. - Use
attempt*when exceptions are expected failures rather than defects.

