Capabilities (CAPS)
The CAPS pattern lets you decouple your workflows from specific record types. Instead of saying “I need the AppEnv record,” a workflow says “I need any environment that provides an IClock.”
This is achieved using the Needs<'dep> interface and the Env<'dep> request token.
Requesting a Dependency with Env<'dep>
In a CAPS workflow, you use the Env<'dep> token to request a dependency by its type. FsFlow handles finding that dependency in the environment for you.
let getTime =
flow {
// Request the IClock dependency directly
let! clock = Env<IClock>
return clock.UtcNow()
}
Projecting with Env<'dep>
Just like Flow.read in the Record Pattern, you can project a value or call a method directly from the dependency using a lambda or the _.Field shorthand.
let readTime =
flow {
// Project UtcNow from the IClock dependency
let! now = Env<IClock> (fun clock -> clock.UtcNow())
// Or use the shorthand
let! now2 = Env<IClock> _.UtcNow
return now
}
Satisfying the Contract with Needs<'dep>
To run a CAPS workflow, your environment must implement the Needs<'dep> interface. This acts as the “glue” between your concrete implementation and the workflow’s requirements.
type MyRuntime =
{ ClockService: IClock }
interface Needs<IClock> with
member x.Dep = x.ClockService
Named Cap Sets
For complex boundaries, you can group multiple dependencies into a single named interface. This is common for “Use Case” boundaries or library APIs.
type LoginCaps =
inherit Needs<IUserStore>
inherit Needs<IClock>
abstract UserStore : IUserStore
abstract Clock : IClock
let login email : Flow<#LoginCaps, AppError, Session> =
flow {
let! store = Env<IUserStore>
let! clock = Env<IClock>
let! user = store.FindByEmail email
let! now = clock.UtcNow()
return { Email = user; IssuedAt = now }
}
Using Flexible Types (#)
By using the #LoginCaps constraint (the “flexible” or “hash” type), this workflow can accept any environment that implements LoginCaps, or even a larger runtime that just happens to satisfy the requirements.
Running on a Larger Runtime
A runtime can be much larger than the specific cap set the flow asks for.
type AppRuntime =
{ UserStoreService : IUserStore
ClockService : IClock
LoggerService : ILogger
Database : IDatabase }
interface LoginCaps with
member x.UserStore = x.UserStoreService
member x.Clock = x.ClockService
interface Needs<IUserStore> with
member x.Dep = x.UserStoreService
interface Needs<IClock> with
member x.Dep = x.ClockService
// Even though AppRuntime is huge, it can run the login flow
login "ada@example.com"
|> Flow.run appRuntime CancellationToken.None
Testing Stays Small
For tests, you only need to implement the specific caps the flow requires, rather than mocking an entire application record.
type TestRuntime =
{ Clock : IClock }
interface Needs<IClock> with member x.Dep = x.Clock
getTime |> Flow.run { Clock = MockClock() } CancellationToken.None
Next Steps
If you want to use simpler record-based environments without interfaces, see the Record Pattern (Environment Slicing) guide.