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.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 appLayerUse 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
}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 }
}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 rightExample
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 }
}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 }
}let appLayer =
layer {
let! runtime = BaseRuntime.fromServiceProvider
and! orders = ordersLayer
return { Runtime = runtime; Orders = orders }
}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 })type AppEnv =
{ Runtime: BaseRuntime
Orders: IOrderRepository }
interface IHasClock with member this.Clock = this.Runtime.Clock
interface IHasOrders with member this.Orders = this.OrdersLayer.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)
