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.JavaScriptComposing 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
AxialLayerslet appFlow : Flow<AppEnv, AppError, unit> =
placeOrder order
let runnable : Flow<IServiceProvider, AppError, unit> =
appFlow |> Layer.provide appLayerPrimary 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 rightUse 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.OrdersThis 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.

