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.JavaScriptProviding Services from a Package
Application code owns both sides of its environment: the workflow names AppEnv, and the composition root supplies
one. A package author has neither. Your library is compiled before its callers exist, so it cannot mention their
types, and it should not force every consumer into one record shape.
This page is the authoring side of service contracts. Everything Axial's own service packages do, you can do.
The shape
Three declarations per service, and the third is the only one with any subtlety.
The service: an ordinary interface describing the capability:
Shared setup
// Setup for the checked examples on this page.
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
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual
SystemIOThreadingTasksAxialLayersConsoleFileSystemHostingBrowserNodePlatformServiceStateTelemetryJavaScriptshouldEqual: 'a -> 'a -> unitexpected: 'aactual: 'a(<>): 'T -> 'T -> boolStructural inequality The first parameter. The second parameter. The result of the comparison. 5 <> 5 // Evaluates to false 5 <> 6 // Evaluates to true [1; 2] <> [1; 2] // Evaluates to false
failwithf: Printf.StringFormat<'T,'Result> -> 'TPrint to a string buffer and raise an exception with the given result. Helper printers must return strings. The formatter. The formatted result. See Printf.failwithf (link: ) for examples.
type IExchangeRates =
abstract GetUsdToAud : unit -> Task<decimal>
FsLiveDocsGeneratedPage8_7555C511A9CE.IExchangeRatesGetUsdToAud: IExchangeRates -> unit -> Task<decimal>unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
decimalAn abbreviation for the CLI type . Basic Types
The contract: how an environment advertises that it supplies one. Named IHasFoo, exposing exactly one member
Foo:
type IHasExchangeRates =
abstract ExchangeRates : IExchangeRates
FsLiveDocsGeneratedPage8_7555C511A9CE.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRatesFsLiveDocsGeneratedPage8_7555C511A9CE.IExchangeRatesThe accessor: one module-level binding that reads it:
[<RequireQualifiedAccess>]
module ExchangeRates =
let service<'env, 'error when 'env :> IHasExchangeRates> : Flow<'env, 'error, IExchangeRates> =
Flow.envWith _.ExchangeRates
Microsoft.FSharp.Core.RequireQualifiedAccessAttributeThis attribute is used to indicate that references to the elements of a module, record or union type require explicit qualified access. Attributes
FsLiveDocsGeneratedPage8_7555C511A9CE.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>enverrorFsLiveDocsGeneratedPage8_7555C511A9CE.IHasExchangeRatesAxial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
FsLiveDocsGeneratedPage8_7555C511A9CE.IExchangeRatesAxial.FlowenvWith: ('env -> 'value) -> Flow<'env,'error,'value>Projects one value from the current environment. This is the primary way to access app dependencies, configuration, or request metadata stored in env. The projection runs only when the flow is executed, so constructing the flow is still pure and side-effect free. Prefer small projections over passing a large environment deeper into reusable helpers. A function that extracts a value from the environment. A containing the projected value. let currentTime () = Flow.envWith (fun (environment: BaseRuntime) -> environment.Clock.UtcNow())
_arg1: 'envExchangeRates: IExchangeRatesBind the accessor at module level, not inline. Flow.envWith _.ExchangeRates cannot resolve inside a flow { } block,
because the lambda's parameter type is not known until the surrounding annotation is applied, which happens after
the body is checked. At module level the annotation sits next to the expression that needs it, so it resolves once
and every caller binds it with no annotation at all.
Everything the package publishes then builds on the accessor:
let priceInAud<'env, 'error when 'env :> IHasExchangeRates> (usdAmount: decimal) : Flow<'env, 'error, decimal> =
flow {
let! rates = ExchangeRates.service
let! rate = ColdTask(fun _ -> rates.GetUsdToAud())
return usdAmount * rate
}
priceInAud: decimal -> Flow<'env,'error,decimal>enverrorFsLiveDocsGeneratedPage8_7555C511A9CE.IHasExchangeRatesusdAmount: decimaldecimalAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Represents a cold workflow that reads an environment, returns a typed result, and is executed explicitly through one of its execution members such as ToTask, ToAsync, or RunSynchronously. The type of the environment dependency. The type of the failure value. The type of the success value.
flow: FlowBuilderThe universal flow { } computation expression.
rates: IExchangeRatesFsLiveDocsGeneratedPage8_7555C511A9CE.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>rate: decimalColdTaskGetUsdToAud: unit -> Task<decimal>(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
Any environment that implements the contract can run it:
type RatesEnv =
{ Rates: IExchangeRates }
interface IHasExchangeRates with
member this.ExchangeRates = this.Rates
FsLiveDocsGeneratedPage8_7555C511A9CE.RatesEnvRates: IExchangeRatesFsLiveDocsGeneratedPage8_7555C511A9CE.IExchangeRatesFsLiveDocsGeneratedPage8_7555C511A9CE.IHasExchangeRatesthis: RatesEnvExchangeRates: RatesEnv -> unit -> IExchangeRatespriceInAud 10m
|> Flow.run { Rates = { new IExchangeRates with member _.GetUsdToAud() = Task.FromResult 1.5m } }
|> shouldEqual (Exit.Success 15.0m : Exit<decimal, string>)
priceInAud: decimal -> Flow<'env,'error,decimal>(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Axial.Flowrun: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
Rates: IExchangeRatesFsLiveDocsGeneratedPage8_7555C511A9CE.IExchangeRates_: IExchangeRatesGetUsdToAud: unit -> Task<decimal>System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
decimalAn abbreviation for the CLI type . Basic Types
stringAn abbreviation for the CLI type . Basic Types
Rules that keep contracts composable
One member per contract, named after the suffix. IHasFoo exposes Foo. This is what makes Flow.envWith _.Foo
predictable and keeps a consumer's composition root readable when it implements six of them.
Never inherit a generic interface. F# rejects a type parameter constrained by two instantiations of the same generic interface, so a generic parent makes your contract impossible to combine with any other, including one from a different package. A contract inherits nothing, or inherits other plain contracts.
type IHasRates = inherit IServiceContract<IExchangeRates> // do not do this
type IHasRates = abstract ExchangeRates : IExchangeRates // do thisMember names may collide freely. Two packages can both define IHasClient exposing Client, and one record can
implement both. F# interface implementations are always explicit, so there is no ambiguity, and package authors do
not need to coordinate names.
Typed errors belong in the package
The reason to publish operations rather than just the interface is that you can wrap the failure model once. Compare
the raw interface call with what Axial.FileSystem publishes:
fileSystem.ReadAllText path // string, throws
FileSystem.readAllText path // Flow<'env, FileSystemError, string>The second is the first plus Flow.catch, classifying exceptions into a union the caller can match on. That
translation is the package's job: the package does it once, and every consumer gets typed failures.
Also expose the raw service
Publish the accessor (ExchangeRates.service) as part of the public API. Callers occasionally need the interface
itself for interop, and without it there is no way to reach it once the environment is contract-based. Axial's own
packages all do this.

