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

Composing Layers

A Layer<'input, 'error, 'output> builds an environment or service bundle from an input value. It runs inside a Scope, so resources acquired during provisioning can be finalized when the provided flow finishes.

open Axial
open Axial.Layers
let appFlow : Flow<AppEnv, AppError, unit> =
    placeOrder order

let runnable : Flow<IServiceProvider, AppError, unit> =
    appFlow |> Layer.provide appLayer
## Primary Shape

Use layer { } for application environment construction:

let! binds a layer's output to the name on its left. do! binds a layer returning unit, while return! uses another layer as the block's result. Sibling and! bindings build independent layers in parallel.

layer {
    let! config = configLayer
    let! orders = ordersLayerFromConfig config
    and! clock = clockLayer
    return! appLayerFrom config orders clock
}
Here is the same block with the left- and right-hand types shown:
layer {
    let! (config: Config) =
        (configLayer: Layer<IServiceProvider, AppError, Config>)

    let! (orders: IOrderRepository) =
        (ordersLayerFromConfig config:
            Layer<IServiceProvider, AppError, IOrderRepository>)

    and! (clock: IClock) =
        (clockLayer: Layer<IServiceProvider, AppError, IClock>)

    return!
        (appLayerFrom config orders clock:
            Layer<IServiceProvider, AppError, AppEnv>)
}
// Layer<IServiceProvider, AppError, AppEnv>
let appLayer =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return { Runtime = runtime; Orders = orders }
    }
Plain `let!` is sequential and dependent. Sibling `and!` bindings are independent and use `Layer.merge`, which provisions branches in parallel through child scopes.

Layer Surface

The core layer surface is:

Layer.succeed value
Layer.envWith projection
Layer.fromValueTask provision
Layer.map mapper layer
Layer.mapError mapper layer
Layer.bind binder layer
Layer.zip left right
Layer.zipPar left right
Layer.merge left right
Layer.map2 mapper left right
Layer.map3 mapper left middle right
Use `Layer.succeed` for already-built values, `Layer.fromValueTask` when construction can fail or register cleanup, and `Layer.bind` / `layer { let! }` when the next provisioning step depends on an earlier value.

Example

open System.Threading.Tasks

type AppEnv =
    { Runtime: BaseRuntime
      Orders: IOrderRepository }

    interface IHasClock with member this.Clock = this.Runtime.Clock
    interface IHasLog with member this.Log = this.Runtime.Log
    interface IHasOrders with member this.Orders = this.Orders

let ordersLayer : Layer<IServiceProvider, BaseRuntimeError, IOrderRepository> =
    Layer.fromValueTask (fun (provider, _) _ ->
        match provider.GetService(typeof<IOrderRepository>) with
        | null ->
            ValueTask(Exit.Failure (Cause.Fail (BaseRuntimeError.MissingService "IOrderRepository")))
        | service ->
            ValueTask(Exit.Success (service :?> IOrderRepository)))

let appLayer : Layer<IServiceProvider, BaseRuntimeError, AppEnv> =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return
        { Runtime = runtime
          Orders = orders }
    }
Layer error types must match the flow error type. When different provisioning steps use different errors, map them into one startup error type before calling `Layer.provide`.

let! And and!

Use let! when provisioning is dependent:

let ordersLayerFromConfig config : Layer<IServiceProvider, BaseRuntimeError, IOrderRepository> =
    Layer.fromValueTask (fun (provider, scope) cancellationToken ->
        // Build or resolve the repository from config, provider, and scope.
        provisionOrders config provider scope cancellationToken)

let appLayer =
    layer {
        let! config = configLayer
        let! orders = ordersLayerFromConfig config

        return { Orders = orders }
    }
Use sibling `and!` when provisioning is independent:
let appLayer =
    layer {
        let! runtime = BaseRuntime.fromServiceProvider
        and! orders = ordersLayer

        return { Runtime = runtime; Orders = orders }
    }
An `and!` sibling cannot depend on a value introduced by another sibling. That remains an ordinary F# compile-time scope error, which is the desired signal: if a service needs another value, separate it into a prior `let!`.

zip, zipPar, And merge

Layer.zip provisions left then right, sequentially. Use it when ordering is intentional.

Layer.zipPar provisions both sides independently in parallel and returns a tuple.

Layer.merge is the layer-domain name for zipPar. Prefer it when combining service bundles or environment fragments:

let combined =
    Layer.merge runtimeLayer ordersLayer
    |> Layer.map (fun (runtime, orders) -> { Runtime = runtime; Orders = orders })
`Layer.merge` does not automatically merge service contracts or synthesize a new environment type. It only provisions both sides and returns their outputs. Keep the final environment explicit:
type AppEnv =
    { Runtime: BaseRuntime
      Orders: IOrderRepository }

    interface IHasClock with member this.Clock = this.Runtime.Clock
    interface IHasOrders with member this.Orders = this.Orders
This keeps service requirements visible to people, the compiler, and LLMs. It also avoids ambiguous cases such as two services with the same implementation type. If an application needs multiple instances of the same service shape, give them named record fields or distinct nominal contracts rather than relying on tags.

Layer.map2 and Layer.map3 are sequential mapping helpers that avoid nested tuple reshaping. In a computation expression, sibling and! bindings use merge instead.

Cleanup

Layer.provide creates a root scope, builds the layer, runs the downstream flow, and closes the scope. Cleanup runs when the layer fails, the downstream flow fails, or the downstream flow succeeds.

Use Layer.acquireRelease when a layer provisions a service implementation or resource that must live for the whole provided flow:

let connectionLayer =
    Layer.acquireRelease
        (Layer.fromValueTask (fun (connectionString, _) _ ->
            openConnection connectionString
            |> Execution.ofValue))
        (fun connection _ ->
            connection.Dispose()
            Task.CompletedTask)
Parallel layer composition uses parent-owned child scopes. If one branch fails after another branch acquired resources, the acquired branch is finalized when the root scope closes. If both parallel branches fail, Axial preserves both failures as `Cause.Both (leftCause, rightCause)` rather than discarding one side.