Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonThe result computation expression
Reified provides a DSL for more concise Result pipelines. result { } passes each success to the next step and
returns the first Error:
open System
open Reified.Result
open Reified.ResultDSL
type SignupError =
| NameMissing
| AgeNotANumber of string
| AgeOutOfRange of int
type Signup = { Name: string; Age: int }
SystemReifiedResultReified.ResultDSLThe concise result vocabulary: the result { } computation expression, its accumulating result.list { } / result.array { } variants, and the lightweight admission functions (okIf, failIf, require, orError, mapError). Optional and opt-in, in the same shape as Reified.DataDSL, Reified.ConstraintDSL, and Reified.SchemaDSL: open Reified.Result for Result, then open Reified.ResultDSL for this vocabulary. Deliberately small: generic combinators such as map, bind, orElse, tap, and the traversal helpers stay qualified as Result.map, Result.bind, and so on.
03-result-handling_70-result-ce.md_page.SignupErrorNameMissingAgeNotANumberstringAn abbreviation for the CLI type . Basic Types
AgeOutOfRangeintAn abbreviation for the CLI type . Basic Types
03-result-handling_70-result-ce.md_page.SignupName: stringAge: intWhat the keywords do
let! binds the value inside Ok to the name on its left. do! runs a step whose success value is unit, so there
is no name. return wraps a plain value back up as Ok. return! uses a complete Result as the block's result.
result {
let! name = parseName "Ada"
let! age = parseAge "36"
return { Name = name; Age = age }
}Ok { Name = "Ada"; Age = 36 }
The same block with the types written out, to show what is on each side of the binding:
result {
let! (name: string) = (parseName "Ada": Result<string, SignupError>)
let! (age: int) = (parseAge "36": Result<int, SignupError>)
return { Name = name; Age = age }
}
// Result<Signup, SignupError>On the right of let! is a Result<'value, 'error>; on the left is the 'value. The block's own type is
Result<'whatever you return, 'error> — the error type is shared by every step, which is why they all fail with
SignupError here.
Failure stops the block
let signup name age =
result {
let! name = parseName name
let! age = parseAge age
return { Name = name; Age = age }
}
signup "Ada" "36" // Ok { Name = "Ada"; Age = 36 }
signup "" "36" // Error NameMissing
signup "Ada" "abc" // Error (AgeNotANumber "abc")signup "" "abc" returns Error NameMissing. The age is never parsed, so its failure is never seen — the block stops
at the first one.
That short-circuit is observable, not just a description of the result:
let mutable calls = 0
let track raw =
calls <- calls + 1
parseAge raw
result {
let! first = parseAge "abc"
let! second = track "36"
return first + second
}
// Error (AgeNotANumber "abc")
calls
// 0track never ran.
Control flow inside a block
The builder supports the ordinary constructs, so a block is not restricted to a straight run of bindings:
result {
use reader = openReader path // disposed on the way out, success or failure
let! header = readHeader reader
for line in lines do // stops at the first failing iteration
do! validate line
while not (isDone ()) do
do! step ()
return header
}try/with and try/finally work as usual. Note what they do and do not catch: they handle .NET exceptions, not
Error values. An Error is an ordinary return value, so it does not trigger with — it just ends the block.
When to use it
- a later step needs a value bound by an earlier one;
- continuing after a failure makes no sense;
- the pipeline has grown past two or three steps and the nesting from
bindis getting hard to read.
When the steps are independent of each other and the caller should hear about all the failures rather than the first, use collecting every error instead.