Getting Started
FsFlow is a toolkit for building robust, Result-based programs in F#. It allows you to scale from simple validation logic to complex, effectful application boundaries using a single, unified mental model.
1. The Continuum of Logic
FsFlow is designed around a continuum. You should always use the simplest tool that satisfies your current requirement:
Pure Checks -> Result & Validation -> Flow
- Pure Checks: Reusable predicates for basic validation.
- Result & Validation: Domain logic that handles success or failure (either fail-fast or error-accumulating).
- Flow: The application boundary where you need dependencies, async/task interop, logging, or cancellation.
2. Start with Checks and Results
Most logic starts pure. Use Check for reusable predicates and Result for domain logic.
open FsFlow
open FsFlow.Check
type UserError = | NameTooShort
let validateName (name: string) : Result<string, UserError> =
name
|> minLength 3
|> orError NameTooShort
// This is just a standard F# Result. No magic yet.
let result = validateName "Ad" // Error NameTooShort
3. Moving to Flow
When your logic needs to interact with the outside world—by calling a database, reading an environment variable, or performing an async task—you move to Flow.
A Flow<'env, 'error, 'value> is a description of a computation. It doesn’t do anything until you run it.
let greetUser (id: int) : Flow<unit, UserError, string> =
flow {
// You can bind a Result directly!
let! name = validateName "Adam"
// You can perform Async or Task work directly!
let! (data: string) = async { return $"Hello {name}" }
return data
}
4. Execution: Turning Description into Action
Because a Flow is just a description, you must explicitly run it. This is the boundary where your platform-independent logic meets the real world.
When you call Flow.run, you provide the required environment (which can be () if none is needed) and a cancellation token is handled for you (defaulting to CancellationToken.None).
Execution Handle vs. Outcome
Because a Flow is just a description, you must explicitly run it. FsFlow handles the platform differences for you:
Flow.run returns an Effect<'value, 'error>. The platform-specific carrier is defined by the target:
- On .NET:
Effect<'value, 'error>is aValueTask<Exit<'value, 'error>>. - On Fable:
Effect<'value, 'error>is anAsync<Exit<'value, 'error>>.
The Exit Outcome
The final result of any flow is an Exit<'value, 'error>. This type represents every possible outcome:
match exitValue with
| Exit.Success value ->
printfn "Success: %A" value
| Exit.Failure (Cause.Fail error) ->
printfn "Expected domain error: %A" error
| Exit.Failure (Cause.Die ex) ->
printfn "Unexpected defect: %s" ex.Message
| Exit.Failure Cause.Interrupt ->
printfn "The workflow was cancelled."
5. Running Your First Flow
Here is how you actually execute a flow in a real application:
let myFlow = Flow.succeed "Hello World"
// On .NET:
let exit = Flow.run () myFlow
// On Fable:
let runOnFable () = async {
let! exit = Flow.run () myFlow
match exit with
| Exit.Success s -> printfn "%s" s
| _ -> ()
}
6. Reading from the Environment
One of Flow’s greatest strengths is managing dependencies without manual parameter passing.
type AppConfig = { ApiUrl: string }
let fetchFromApi : Flow<AppConfig, unit, string> =
flow {
// Read just the ApiUrl from the environment record
let! url = Flow.read _.ApiUrl
return $"Fetching from {url}..."
}
// Running with an environment
let config = { ApiUrl = "https://api.example.com" }
let effect = Flow.run config fetchFromApi
Summary: The Flow Lifecycle
- Define: Use
flow {}to describe your logic and its requirements. - Compose: Combine smaller flows, Results, Tasks, and Asyncs into larger ones.
- Run: Call
Flow.run envat your application’s entry point (e.g., a Controller or Main function). - Handle: Match on the
Exitvalue to handle success, failure, or defects.
Next Steps
- Managing Dependencies: Learn how to structure your environments using the Record or CAPS patterns.
- Execution Semantics: Understand short-circuiting, “cold” vs “hot” tasks, and interruption.
- Task and Async Interop: A deep dive into binding different effect types.