Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultCollecting every error
result { } stops at the first failure. For independent steps such as fields of a form or columns of a row, callers may need every failure at once.
result.list { } collects them. Join the independent bindings with and! instead of let!.
open System
open Reified.Result
open Reified.ResultDSL
type SignupError =
| NameMissing
| AgeNotANumber of string
| AgeOutOfRange of int
type Signup = { Name: string; Age: int }
let parseName (name: string) : Result<string, SignupError> =
if String.IsNullOrEmpty name then Error NameMissing else Ok name
let parseAge (age: string) : Result<int, SignupError> =
match Int32.TryParse age with
| true, value -> Ok value
| false, _ -> Error(AgeNotANumber age)
let signupAll name age =
result.list {
let! name = parseName name
and! age = parseAge age
return { Name = name; Age = age }
}
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.
FsLiveDocsGeneratedPage18_9A5DDEA924C8.SignupErrorNameMissingAgeNotANumberstringAn abbreviation for the CLI type . Basic Types
AgeOutOfRangeintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage18_9A5DDEA924C8.SignupName: stringAge: intparseName: string -> Result<string,SignupError>name: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
System.StringRepresents text as a sequence of UTF-16 code units.
IsNullOrEmpty: string -> boolIndicates whether the specified string is or an empty string (""). The string to test. if the parameter is or an empty string (""); otherwise, .
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
parseAge: string -> Result<int,SignupError>age: stringSystem.Int32Represents a 32-bit signed integer.
TryParse: string * byref<int> -> boolConverts the string representation of a number to its 32-bit signed integer equivalent. A return value indicates whether the conversion succeeded. A string containing a number to convert. When this method returns, contains the 32-bit signed integer value equivalent of the number contained in , if the conversion succeeded, or zero if the conversion failed. The conversion fails if the parameter is or , is not of the correct format, or represents a number less than Int32.MinValue or greater than Int32.MaxValue. This parameter is passed uninitialized; any value originally supplied in will be overwritten. if was converted successfully; otherwise, .
value: intsignupAll: string -> string -> Result<Signup,SignupError list>result: ResultBuilderThe fail-fast result { } computation expression.
list: ListResultBuilderThe applicative builder collecting errors into a list: result.list { }.
age: intRequires & types
Requires: open Reified.ResultDSL; open System (packages: Reified.Result)val parseName: string -> Result<string,SignupError>val parseAge: string -> Result<int,SignupError>val signupAll: string -> string -> Result<Signup,SignupError list>Both bindings run, whatever the other one does:
signupAll "Ada" "36" // Ok { Name = "Ada"; Age = 36 }
signupAll "" "abc" // Error [NameMissing; AgeNotANumber "abc"]
signupAll "" "36" // Error [NameMissing]
signupAll: string -> string -> Result<Signup,SignupError list>The error type changed. Each step still fails with a single SignupError, but the block collects them, so it returns
Result<Signup, SignupError list>. Errors appear in binding order.
Choosing the container
The builder name picks the container, and it shows up in the block's type:
| Builder | Block type |
|---|---|
result.list { } |
Result<'value, 'error list> |
result.array { } |
Result<'value, 'error[]> |
result.array {
let! name = parseName ""
and! age = parseAge "abc"
return { Name = name; Age = age }
}
// Error [|NameMissing; AgeNotANumber "abc"|]
result: ResultBuilderThe fail-fast result { } computation expression.
array: ArrayResultBuilderThe applicative builder collecting errors into an array: result.array { }.
name: stringparseName: string -> Result<string,SignupError>age: intparseAge: string -> Result<int,SignupError>Name: stringAge: intRequires & types
Requires: open Reified.ResultDSL (packages: Reified.Result)For any other container, map at the end, Result.mapError Set.ofList, and so on. NonEmptyList.ofList (from
Reified.Refinements) returns an option because the source list may be empty, so wrap it in a local total
conversion rather than passing it straight to mapError:
open Reified.Refinements
let errorsOr (fallback: NonEmptyList<'error>) (errors: 'error list) : NonEmptyList<'error> =
errors |> NonEmptyList.ofList |> Option.defaultValue fallback
ReifiedRefinementserrorsOr: NonEmptyList<'error> -> 'error list -> NonEmptyList<'error>fallback: NonEmptyList<'error>Reified.Refinements.NonEmptyList`1A list that contains at least one item. The case is public: non-emptiness is carried by the representation rather than by a checked constructor, so head, last, reduce, min, and max are total and pattern matching is available to callers.
errorerrors: 'error listlistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.Refinements.NonEmptyListModuleOperations over lists that carry their non-emptiness in the type.
ofList: 'a list -> NonEmptyList<'a> optionReturns the non-empty list, or None when the source is empty.
Microsoft.FSharp.Core.OptionModuleContains operations for working with options. Options
defaultValue: 'T -> 'T option -> 'TGets the value of the option if the option is Some, otherwise returns the specified default value. The specified default value. The input option. The option if the option is Some, else the default value. Identical to the built-in operator, except with the arguments swapped. (99, None) ||> Option.defaultValue // evaluates to 99 (99, Some 42) ||> Option.defaultValue // evaluates to 42
Requires & types
Requires: open Reified.Refinements (packages: Reified.Refinements)val errorsOr: NonEmptyList<'error> -> 'error list -> NonEmptyList<'error>and! accumulates; let! still fails fast
Both keywords work in the same block and they mean different things. Bindings joined by and! are independent, so all
of them run and their errors combine. A let! that follows depends on what came before, so it cannot run until those
bindings have succeeded.
let mixed name age =
result.list {
let! name = parseName name
and! age = parseAge age // independent: both always run
let! confirmed = // dependent: needs age
if age >= 18 then Ok age else Error (AgeOutOfRange age)
return { Name = name; Age = confirmed }
}
mixed: string -> string -> Result<Signup,SignupError list>name: stringage: stringresult: ResultBuilderThe fail-fast result { } computation expression.
list: ListResultBuilderThe applicative builder collecting errors into a list: result.list { }.
parseName: string -> Result<string,SignupError>age: intparseAge: string -> Result<int,SignupError>confirmed: int(>=): 'T -> 'T -> boolStructural greater-than-or-equal The first parameter. The second parameter. The result of the comparison. 5 >= 1 // Evaluates to true 5 >= 5 // Evaluates to true [1; 5] >= [1; 6] // Evaluates to false
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
AgeOutOfRangeName: stringAge: intRequires & types
Requires: open Reified.ResultDSL (packages: Reified.Result)val mixed: string -> string -> Result<Signup,SignupError list>mixed "Ada" "36" // Ok { Name = "Ada"; Age = 36 }
mixed "" "abc" // Error [NameMissing; AgeNotANumber "abc"]
mixed "Ada" "12" // Error [AgeOutOfRange 12]
mixed: string -> string -> Result<Signup,SignupError list>In the third case, name and age succeeded, then let! failed with one error. In the second case, let! never ran,
so AgeOutOfRange could not appear alongside the other errors.
The rule: everything you want reported together must be in the same and! group. A failure in an earlier group
hides every later one.
Composing blocks
A binding whose error type is already the collected container passes through unchanged, and a single-error binding is lifted to a one-error list before both join. The output of one accumulating block can therefore feed another without nesting:
let already: Result<int, SignupError list> = Error [ NameMissing ]
result.list {
let! first = already
and! second = parseAge "abc"
return first + second
}
// Error [NameMissing; AgeNotANumber "abc"]
already: Result<int,SignupError list>Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
intAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage18_9A5DDEA924C8.SignupErrorlistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
NameMissingresult: ResultBuilderThe fail-fast result { } computation expression.
list: ListResultBuilderThe applicative builder collecting errors into a list: result.list { }.
first: intsecond: intparseAge: string -> Result<int,SignupError>(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "Word" // Evaluates to "Hello World"
Requires & types
Requires: open Reified.ResultDSL (packages: Reified.Result)val already: Result<int,SignupError list>already arrives as Result<int, SignupError list> and is used unchanged; parseAge "abc" arrives as
Result<int, SignupError> and is lifted to Error [AgeNotANumber "abc"]. The two are appended into one flat list
rather than nested.
What it does not do
The collected errors are a flat container. They carry no field names, no paths, and no indication of which binding produced which error, only the order they were bound in.
If the independent bindings are fields of structured input, a form post, a request body, a configuration file, that
association is Schema's job, and it is the default next step. Declare the model once and Schema.parse accumulates
every field failure with the path that produced it, keeps the raw value for redisplay, renders messages in the
caller's language, and hands back your domain type:
open Reified
open Reified.SchemaDSL
open Reified.ConstraintDSL
type Signup = { Name: string; Age: int }
let signupSchema =
schema<Signup> {
field _.Name { constrain Constraint.present }
field _.Age { constrain (Constraint.atLeast 18) }
construct (fun name age -> { Name = name; Age = age })
}
let form = [ "name", ""; "age", "12" ]
match Data.ofNameValues form |> Schema.parse signupSchema with
| Ok signup -> printfn "%A" signup
| Error errors ->
for issue in SchemaErrors.toList errors do
printfn "%s: %s" (SchemaPath.format issue.Path) (SchemaError.render issue.Error)
ReifiedReified.SchemaDSLThe concise schema-definition vocabulary: the record computation expression, its field and constructor forms, and the collection-schema operations. Optional and opt-in, in the same shape as Reified.DataDSL and Reified.ConstraintDSL: open Reified.Schema for Schema, then open Reified.SchemaDSL for this vocabulary. There is no constraint catalogue here. One Constraint vocabulary serves direct checking, refinement, and Schema, so a field block reaches for Constraint.email or an opened Reified.ConstraintDSL exactly as standalone code does. Boundary supply is Schema-owned and stays here as mustSupply and mayOmit.
Reified.ConstraintDSLConstraint constructors usable without the Constraint. prefix inside a module that declares value rules. Optional vocabulary, not another abstraction. Opening it makes a declaration read minLength 3 instead of Constraint.minLength 3; everything here is the same value the qualified name returns. Some constructors are deliberately left out because they shadow names the same validation code is likely to need in scope: contains, distinct, all, any, length, and between shadow core F# operations. Reach for those as Constraint.contains, Constraint.all, and so on, even inside a module that has opened this DSL. Constraint execution (Constraint.satisfies, Constraint.check, Constraint.guard) is likewise always qualified: ConstraintDSL declares constraints, Constraint.* executes and inspects them. orError and mapError are structural adapters matching the corresponding Result operations. They let a constraint pipeline retain its input and finish with the application's error type without adding an Reified.Result dependency. module SignupRules = open Reified.ConstraintDSL let age : Constraint<int> = atLeast 13 let contact : Constraint<string> = Constraint.all [ present; email ] let requireContact value = value |> Constraint.guard contact |> orError EmailRequired
FsLiveDocsGeneratedPage19_9A5DDEA924C8.SignupName: stringstringAn abbreviation for the CLI type . Basic Types
Age: intintAn abbreviation for the CLI type . Basic Types
signupSchema: Schema<Signup>schema: SchemaCeBuilder.SchemaBuilder<'model>Record-schema computation expression.
``.ctor``: Quotations.Expr<(Signup -> string)> -> field<Signup,string>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
``.ctor``: Quotations.Expr<('model -> 'target)> -> field<'model,'target>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
_arg1: SignupConstrain: SchemaFieldSteps.FieldInitial<Signup,string> * Constraint<string> -> SchemaFieldSteps.FieldConfigured<Signup,string>Adds a portable constraint to the field's current schema value.
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
present: Constraint<^value>Requires a value to be inhabited according to its shape. Whitespace-only text is blank, as are null text, a null or empty collection or map, None, ValueNone, and an empty Nullable. Blankness means .NET's whitespace set plus U+FEFF, which is what lets the rule be exported; see nonBlankPattern. The shape is selected from the return type, so a reusable binding needs its annotation: let requiredName : Constraint<string> = Constraint.present. Applied where the type is already known — inside an annotated rule, or to a schema — no annotation is needed. let requiredName : Constraint<string> = Constraint.present
``.ctor``: Quotations.Expr<(Signup -> int)> -> field<Signup,int>Declares a field, deriving its camel-cased wire name from the property getter. type Signup = { Email: string } field (fun (s: Signup) -> s.Email) // wire name "email"
_arg2: SignupConstrain: SchemaFieldSteps.FieldInitial<Signup,int> * Constraint<int> -> SchemaFieldSteps.FieldConfigured<Signup,int>Adds a portable constraint to the field's current schema value.
atLeast: 'value -> Constraint<'value>Requires a value greater than or equal to the supplied bound. let age : Constraint<int> = Constraint.atLeast 13
construct: 'constructor -> SchemaCeBuilder.ConstructorStep<'model,'constructor>Closes a record schema with a total constructor.
name: stringage: intform: (string * string) listReified.DataModuleofNameValues: (string * string) seq -> DataBuilds object-shaped data from name and value pairs.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Reified.SchemaConstruction, composition, parsing, and checking for universal schemas.
parse: Schema<'a> -> Data -> Result<'a,SchemaErrors>Parses source-neutral structured data, runs constraints and refinements, and invokes record constructors.
OkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
signup: Signupprintfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
errors: SchemaErrorsissue: SchemaIssueReified.SchemaErrorsModuleFunctions for inspecting and rendering accumulated schema failures.
toList: SchemaErrors -> SchemaIssue listReturns failures in deterministic path order.
Reified.SchemaPathModuleFunctions for building and formatting schema error paths.
format: SchemaPath -> stringFormats a path with dot-separated keys and bracketed indexes.
Path: SchemaPathThe location of the failure.
Reified.SchemaErrorModuleFunctions for lowering and rendering boundary schema failures.
render: SchemaError -> stringRenders a schema error as an English sentence. Constraint violations delegate to Violation.render, so there is one rendering catalogue. SchemaError.render (SchemaError.InvalidFormat "int") // "Expected int format."
Error: SchemaErrorThe schema failure.
Data.ofNameValues keeps each value as text. Schema.parse reads each field's serialized form, so "12" is parsed
into 12 and then fails atLeast 18, while "" fails present:
age: Expected a value at least 18, but was 12.
name: Value must be present.
Staying at the Result layer is still reasonable when the failures are not really fields, independent checks in application code, or a small internal function whose caller just wants a list. In that case carry the association yourself, by mapping each step's error to a pair of field name and reason before the block collects them.
result.list { ... and! ... } accumulates one group of independent bindings. Schema.parse accumulates
failures at a structured boundary, with paths.
For the third case, the same operation applied across a collection, use
Result.traverseAll and Result.sequenceAll, which run every mapping
and return Result<'output list, 'error list>.