Skip to content

adz/FsFlow

Repository files navigation

FsFlow

Warning

API Still stabilising - wait for 1.0 to avoid breaking changes

FsFlow

FsFlow provides structured composition over normal F#/.NET code. It is a coherent application architecture model for F# on .NET, centered on a unified effect system.

Write small predicate checks with Check, keep fail-fast logic in standard Result, accumulate sibling validation with Validation and validate {}, then lift the same logic into Flow when the boundary needs environment access, async work, task interop, or runtime policy.

ci NuGet License

Coherent Application Architecture

FsFlow is built around one progression:

Check -> Result -> Validation -> Flow

The same vocabulary stays the same while the execution context grows.

  • Structured Composition: A single flow {} builder that binds Result, Option, Async, Task, and ColdTask directly, eliminating the "adapter tax" of switching helper families at every boundary.
  • Architectural Honesty: Keep dependencies explicit in 'env - but allow integration with IServiceProvider at the boundary.
  • ZIO-Style Execution: Preserves the critical distinction between typed domain failures, cancellations, and unhandled defects.
  • Composable State: Built-in Software Transactional Memory (STM) for atomic coordination across multiple variables without manual lock management.

Example

Lets start by showing a reusable check and a fail-fast result:

type RegistrationError =
    | EmailMissing
    | SaveFailed of string

let validateEmail (email: string) : Result<string, RegistrationError> =
    email
    |> Check.whenNotBlank
    |> Check.withError EmailMissing

Use the same validation logic directly inside a task-oriented workflow:

type User =
    { Email: string }

type RegistrationEnv =
    { LoadUser: int -> Task<Result<User, RegistrationError>>
      SaveUser: User -> Task<Result<unit, RegistrationError>> }

let registerUser userId : Flow<RegistrationEnv, RegistrationError, unit> =
    flow {
        let! loadUser = Flow.read _.LoadUser
        let! saveUser = Flow.read _.SaveUser

        let! user = loadUser userId
        do! validateEmail user.Email

        return! saveUser user
    }

validateEmail is just a plain Result<string, RegistrationError>. flow lifts it directly with do!. The same builder also binds Async, Task, ValueTask, and ColdTask directly.

What You Get

FsFlow stays close to standard F# and .NET:

  • flow { ... } binds to Result and Option
  • flow { ... } also binds to Async, Async<Option<_>>, Async<ValueOption<_>>, and Async<Result<_,_>>
  • On .Net, flow { ... } also binds to Task, ValueTask, Task<_>, ValueTask<_>, and ColdTask
  • result {} keeps fail-fast pure code readable
  • validate {} keeps sibling validation accumulation explicit

Because tasks are hot, FsFlow includes ColdTask: a small wrapper around CancellationToken -> Task. flow handles token passing for you and keeps reruns explicit.

A full example

The full runnable example is in examples/FsFlow.ReadmeExample/Program.fs.

dotnet run --project examples/FsFlow.ReadmeExample/FsFlow.ReadmeExample.fsproj
// ReadmeEnv = { Root: string }
// FileReadError = NotFound

let readTextFile (path: string) : Flow<ReadmeEnv, FileReadError, string> =
    flow {
        // In production, map access and path exceptions separately at the boundary.
        do! File.Exists path |> Check.isTrue |> BindError.withError (NotFound path)

        // Wrap in ColdTask for later exeuction
        return! ColdTask(fun ct -> File.ReadAllTextAsync(path, ct))
    }

let program : Flow<ReadmeEnv, FileReadError, string * string> =
    flow {
        let! root = Flow.read _.Root                       // ReadmeEnv.Root -> string
        let settingsFile = Path.Combine(root, "settings.json")
        let featureFlagsFile = Path.Combine(root, "feature-flags.json")

        let! settings = readTextFile settingsFile          // Flow<ReadmeEnv, FileReadError, string>
        let! featureFlags = readTextFile featureFlagsFile  // Flow<ReadmeEnv, FileReadError, string>

        return settings, featureFlags                      // Flow<ReadmeEnv, FileReadError, string * string>
    }

It reads Root from 'env, performs two file reads in one flow {}, and keeps failure typed at the boundary.

Getting Started

About

FsFlow is an F# library for typed results, explicit context, and async/task interop.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors