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.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:
type IExchangeRates =
abstract GetUsdToAud : unit -> Task<decimal>
14-advanced_01-reusable-packages.md_page.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
14-advanced_01-reusable-packages.md_page.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRates14-advanced_01-reusable-packages.md_page.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
14-advanced_01-reusable-packages.md_page.ExchangeRatesservice: Flow<'env,'error,IExchangeRates>enverror14-advanced_01-reusable-packages.md_page.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.
14-advanced_01-reusable-packages.md_page.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 — and that 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 (usdAmount: decimal) : Flow<#IHasExchangeRates, RateError, decimal> =
flow {
let! rates = ExchangeRates.service
let! rate = rates.GetUsdToAud()
return usdAmount * rate
}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 no coordination
needed between package authors.
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 — doing it once is why consumers get typed failures for free.
Also expose the raw service
Publish the accessor (ExchangeRates.service) as part of the public surface. 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.

