FsFlow 0.3.0
dotnet add package FsFlow --version 0.3.0
NuGet\Install-Package FsFlow -Version 0.3.0
<PackageReference Include="FsFlow" Version="0.3.0" />
<PackageVersion Include="FsFlow" Version="0.3.0" />
<PackageReference Include="FsFlow" />
paket add FsFlow --version 0.3.0
#r "nuget: FsFlow, 0.3.0"
#:package FsFlow@0.3.0
#addin nuget:?package=FsFlow&version=0.3.0
#tool nuget:?package=FsFlow&version=0.3.0
FsFlow
API Still stabilising - wait for 1.0 to avoid breaking changes
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/content/img/fsflow-readme-dark.svg"> <source media="(prefers-color-scheme: light)" srcset="docs/content/img/fsflow-readme-light.svg"> <img alt="FsFlow" src="docs/content/img/fsflow-readme-light.svg" width="160"> </picture>
FsFlow is a single model for Result-based programs in F#.
Write validation and typed-error logic once, keep it as plain Result while the code is pure,
then lift the same logic into Flow, AsyncFlow, or TaskFlow when the boundary needs
environment access, async work, task interop, cancellation, or runtime policy.
Core Model
FsFlow is built around one progression:
Validate -> Result -> Flow -> AsyncFlow -> TaskFlow
The validation vocabulary stays the same while the execution context grows.
- Start with plain
Resultand pure validation helpers. - Use
flow {}when the boundary needs typed failure and environment, but not async runtime. - Use
asyncFlow {}when the boundary is naturallyAsync. - Use
taskFlow {}when the boundary is naturally.NET Task. - Keep expected failures typed all the way through instead of switching helper families at each runtime shape.
This is the key difference from split models like Result, Async<Result<_,_>>, and Task<Result<_,_>>
that need separate helper modules, separate builders, and repeated adaptation at the boundary.
Install
FsFlowforFlowandAsyncFlowFsFlow.NetforTaskFlow
Example
Start with pure validation:
open System.Threading.Tasks
open FsFlow.Validate
type RegistrationError =
| EmailMissing
| SaveFailed of string
let validateEmail (email: string) : Result<unit, RegistrationError> =
email
|> okIfNotBlank
|> Result.map ignore
|> orElse EmailMissing
Use the same Result directly inside a task-oriented workflow:
open System.Threading.Tasks
open FsFlow.Net
type User =
{ Email: string }
type RegistrationEnv =
{ LoadUser: int -> Task<Result<User, RegistrationError>>
SaveUser: User -> Task<Result<unit, RegistrationError>> }
let registerUser userId : TaskFlow<RegistrationEnv, RegistrationError, unit> =
taskFlow {
let! loadUser = TaskFlow.read _.LoadUser
let! saveUser = TaskFlow.read _.SaveUser
let! user = loadUser userId
do! validateEmail user.Email
return! saveUser user
}
validateEmail is just Result<unit, RegistrationError>.
taskFlow lifts it directly with do!.
There is no separate task-result validation vocabulary to learn first.
Semantic Boundary
FsFlow is for short-circuiting, ordered workflows:
Validate,Result,Flow,AsyncFlow, andTaskFlowstop on the first typed failure.- They are for orchestration, dependency access, async or task execution, and runtime concerns.
- They are not accumulated validation builders.
If you need accumulated validation, keep that explicit with a dedicated validation library or bridge it in at the edge.
What You Get
FsFlow stays close to standard F# and .NET:
flow { ... }binds toResultandOptionasyncFlow { ... }also binds toAsyncandAsync<Result<_,_>>taskFlow { ... }binds toTask,ValueTask,Task<_>,ValueTask<_>, andColdTaskValidateworks as plainResultlogic before lifting into a workflow
Because tasks are hot, FsFlow includes ColdTask: a small wrapper around CancellationToken -> Task.
taskFlow handles token passing for you and keeps reruns explicit.
This is the file-oriented example shape. The full runnable example is in
examples/FsFlow.ReadmeExample/Program.fs.
dotnet run --project examples/FsFlow.ReadmeExample/FsFlow.ReadmeExample.fsproj --nologo
Supporting types in the full example are just:
ReadmeEnv = { Root: string }FileReadError = NotFound
let readTextFile (path: string) : TaskFlow<ReadmeEnv, FileReadError, string> =
taskFlow {
// In production, map access and path exceptions separately at the boundary.
do! okIf (File.Exists path) |> orElse (NotFound path) // from Validate
return! ColdTask(fun ct -> File.ReadAllTextAsync(path, ct)) // ColdTask<string>
}
let program : TaskFlow<ReadmeEnv, FileReadError, string * string> =
taskFlow {
let! root = TaskFlow.read _.Root // ReadmeEnv.Root -> string
let settingsFile = Path.Combine(root, "settings.json")
let featureFlagsFile = Path.Combine(root, "feature-flags.json")
let! settings = readTextFile settingsFile // TaskFlow<ReadmeEnv, FileReadError, string>
let! featureFlags = readTextFile featureFlagsFile // TaskFlow<ReadmeEnv, FileReadError, string>
return settings, featureFlags // TaskFlow<ReadmeEnv, FileReadError, string * string>
}
It reads Root from 'env, performs two file reads in one taskFlow {}, and keeps failure typed at the boundary.
Getting Started
- Docs site for guides and API reference
docs/VALIDATE_AND_RESULT.mdfor the validation-first storyexamples/for runnable repo examplesdocs/TINY_EXAMPLES.mdfor the smallest runnable snippets
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- FSharp.Core (>= 10.1.203)
-
net8.0
- FSharp.Core (>= 10.1.203)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FsFlow:
| Package | Downloads |
|---|---|
|
FsFlow.Net
.NET task-oriented workflows, task interop, and task-specific runtime helpers for FsFlow. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Split task-oriented workflows into FsFlow.Net while keeping sync and Async core concepts in the FsFlow package.