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 is compiled before its callers exist, so it cannot mention their types or require one record shape.
This page covers the package side of service contracts.
The shape
Declare an interface, a contract (IHasFoo exposing one Foo member), and a module-level accessor. The
reusable services tutorial walks through the complete exchange-rate example — the
IExchangeRates interface, the IHasExchangeRates contract, the ExchangeRates.service accessor, a priceInAud
helper, an app environment, a typed failure, and a test double. This page covers the rules that keep such packages
composable.
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. The record's implementations are explicit — each interface block names the contract it implements
— so two contracts that expose the same member name do not clash and package authors do not need to coordinate
names:
Show page setup
/// 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// Two packages independently define contracts with the same member name.
type IHasClient =
abstract Client : string
type IHasGateway =
abstract Client : string
// One record implements both; each interface block names the contract it implements.
type ClientEnv =
{ ClientValue: string
GatewayValue: string }
interface IHasClient with
member this.Client = this.ClientValue
interface IHasGateway with
member this.Client = this.GatewayValue
FsLiveDocsGeneratedPage1_6B7179067CFE.IHasClientClient: IHasClient -> unit -> stringstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage1_6B7179067CFE.IHasGatewayClient: IHasGateway -> unit -> stringFsLiveDocsGeneratedPage1_6B7179067CFE.ClientEnvClientValue: stringGatewayValue: stringthis: ClientEnvClient: ClientEnv -> unit -> stringlet clientEnv = { ClientValue = "http"; GatewayValue = "db" }
(clientEnv :> IHasClient).Client |> shouldEqual "http"
(clientEnv :> IHasGateway).Client |> shouldEqual "db"
clientEnv: ClientEnvClientValue: stringGatewayValue: stringClient: stringFsLiveDocsGeneratedPage1_6B7179067CFE.IHasClient(|>): '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
shouldEqual: 'a -> 'a -> unitFsLiveDocsGeneratedPage1_6B7179067CFE.IHasGatewayTyped errors belong in the package
Publishing operations lets the package 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>FileSystem.readAllText uses Flow.catch to classify exceptions into a union the caller can match on. The
custom-services tutorial builds the same pattern end to end:
priceInAudOrFail wraps GetUsdToAud with Flow.attemptTask, classifies TimeoutException versus any other
exception through ExchangeRateError.fromException, and runs three checked outcomes (success, ProviderTimedOut,
and RateUnavailable).
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.

