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
type IHasExchangeRates =
abstract ExchangeRates : IExchangeRates
14-advanced_01-reusable-packages.md_page.IHasExchangeRatesExchangeRates: IHasExchangeRates -> unit -> IExchangeRates14-advanced_01-reusable-packages.md_page.IExchangeRates[<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: IExchangeRatesEverything 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
}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 thisTyped 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>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.

