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.JavaScript

Reading the Type

The full Flow type has three parameters:

Fragment
Flow<'env, 'error, 'value>

Read them from left to right:

Parameter Meaning
'env Dependencies supplied when the workflow runs
'error Expected failures the caller can handle
'value The value produced on success

For example:

Fragment
let loadUser (id: UserId) : Flow<AppEnv, LoadUserError, User> = ...

The type does not mean that work has started. A Flow is an immutable description, and building one starts nothing: each execution interprets the description with an environment and produces an outcome. A Flow built from cold sources (the usual case) starts fresh work on every execution. But a Flow can capture work that is already running — Flow.awaitStartedTask observes a task that began before the Flow was built — so "cold" describes when the description starts work, not the lifecycle of everything it holds.

Aliases

Short aliases are abbreviations for the same three-parameter type. Each one fixes the channels it leaves out:

Alias Expands to Meaning
Flow<'value> Flow<unit, Never, 'value> No environment and no typed failure
Flow<'error, 'value> Flow<unit, 'error, 'value> Typed failure, no environment
EnvFlow<'env, 'value> Flow<'env, Never, 'value> Environment, no typed failure
ExnFlow<'value> Flow<unit, exn, 'value> Recoverable exceptions as typed failures
ExnEnvFlow<'env, 'value> Flow<'env, exn, 'value> Environment and recoverable exceptions

unit in the environment channel means the workflow reads nothing. Never is an error type with no values, so a Flow<'value> or EnvFlow<'env, 'value> cannot fail with an expected error. Writing the alias and writing the expansion produce the same type, so the two forms are interchangeable in a signature:

Verified
open Axial
Verified
let render : Flow<string> = Flow.succeed "ok"
let renderExpanded : Flow<unit, Never, string> = render
Requires & types
Requires: open Axial (packages: Axial)
val render: Flow<string>
val renderExpanded: Flow<unit,Never,string>

Use an alias when it shortens a signature without hiding information.

What runs underneath

Running a Flow produces an awaitable outcome. On .NET that is a ValueTask, so work that completes synchronously avoids a heap allocation. On Fable's JavaScript target it is an Async, because JavaScript has no value-type task. Your code does not change between the two: Flow<'env, 'error, 'value> is the same type on both.

On .NET, Flow.bind and Flow.delay hand tail continuations back to an execution loop. A recursive workflow such as let rec loop () = flow { do! step; return! loop () } does not retain a pending task for every iteration. Pending work after a recursive call still needs storage: an enclosing try/finally, for example, must keep its cleanup until the recursive call finishes. Release per-iteration resources before the next recursive call, or use a while loop whose body owns those resources.

See Async and Task interop for binding existing Async, Task, and ValueTask code.

  • Flow API reference maps the construction, environment, composition, execution, resource, and concurrency functions.
  • Troubleshooting Types explains the compiler errors produced when environment or error channels do not line up.