Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultIntroductory Reference App
The introductory app uses Reified.Result, Reified.Constraint, Reified.Refinements, and Reified.Parse without Schema.
Every snippet below is taken from examples/Reified.ReferenceApp.Intro/Program.fs, which compiles and runs:
dotnet run --project examples/Reified.ReferenceApp.Intro/Reified.ReferenceApp.Intro.fsproj --nologo
The examples open the DSL modules, so the constraint and result vocabulary is unqualified:
open Reified
open Reified.Result
open Reified.ResultDSL
open Reified.ConstraintDSL
open Reified.Refinements
ReifiedResultReified.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.
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
RefinementsRequires & types
Requires: (packages: Reified.Constraint, Reified.Result)Reusable checks
Constraint.guard runs a constraint and returns the input on success, so the value survives the check. orError
replaces the Violation with the application's own error case, which keeps the signature in the application's
vocabulary:
type BadgeError =
| NameTooShort
| NameTooLong
/// A badge name must be 3 to 40 characters.
let validateBadgeName (name: string) : Result<string, BadgeError> =
name
|> Constraint.guard (minLength 3)
|> orError NameTooShort
|> Result.bind (Constraint.guard (maxLength 40) >> orError NameTooLong)
FsLiveDocsGeneratedPage12_163BCE492C58.BadgeErrorNameTooShortNameTooLongvalidateBadgeName: string -> Result<string,BadgeError>name: stringstringAn abbreviation for the CLI type . Basic Types
Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
(|>): '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.ConstraintModuleCreates, executes, composes, and inspects constraints.
guard: Constraint<'value> -> 'value -> Result<'value,Violation>Runs a constraint and returns the unchanged value after success. let requiredName : Constraint<string> = Constraint.present "Alice" |> Constraint.guard requiredName |> Result.mapError Violation.render
minLength: int -> Constraint<^value>Alias for .
orError: 'a -> Result<'b,'c> -> Result<'b,'a>Replaces a failed constraint's violation with the supplied error. Defined here because Reified.Constraint does not depend on Reified.Result. value |> Constraint.guard present |> orError NameRequired
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
bind: ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>Binds a result to the next fail-fast operation.
maxLength: int -> Constraint<^value>Alias for .
(>>): ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3Compose two functions, the function on the left being applied first The first function to apply. The second function to apply. The composition of the input functions. let addOne x = x + 1 let doubleIt x = x * 2 let addThenDouble = addOne >> doubleIt addThenDouble 3 // Evaluates to 8
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.Result (packages: Reified.Constraint, Reified.Result)val validateBadgeName: string -> Result<string,BadgeError>Dependent work
result { } sequences steps that depend on each other, so the first failure stops the pipeline:
type TicketError =
| UnknownTier of string
| QuantityNotANumber of string
| QuantityOutOfRange of int
type Tier =
| General
| Speaker
let parseTier (raw: string) : Result<Tier, TicketError> =
match raw.Trim().ToLowerInvariant() with
| "general" -> Ok General
| "speaker" -> Ok Speaker
| other -> Error(UnknownTier other)
let parseTicketRequest (rawTier: string) (rawQuantity: string) : Result<Tier * int, TicketError> =
result {
let! tier = parseTier rawTier
let! quantity = Parse.int rawQuantity |> orError (QuantityNotANumber rawQuantity)
do!
(quantity >= 1 && quantity <= 6)
|> Result.require
|> Result.orError (QuantityOutOfRange quantity)
return tier, quantity
}
FsLiveDocsGeneratedPage12_163BCE492C58.TicketErrorUnknownTierstringAn abbreviation for the CLI type . Basic Types
QuantityNotANumberQuantityOutOfRangeintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage12_163BCE492C58.TierGeneralSpeakerparseTier: string -> Result<Tier,TicketError>raw: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
Trim: unit -> stringToLowerInvariant: unit -> stringOkRepresents an OK or a Successful result. The code succeeded with a value of 'T.
other: stringErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
parseTicketRequest: string -> string -> Result<(Tier * int),TicketError>rawTier: stringrawQuantity: stringresult: ResultBuilderThe fail-fast result { } computation expression.
tier: Tierquantity: intReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
(|>): '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
orError: 'a -> Result<'b,'c> -> Result<'b,'a>Replaces a failed constraint's violation with the supplied error. Defined here because Reified.Constraint does not depend on Reified.Result. value |> Constraint.guard present |> orError NameRequired
(>=): '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
(&&): bool -> bool -> boolBinary 'and'. When used as a binary operator the right hand value is evaluated only on demand The first value. The second value. The result of the operation.
(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
require: bool -> Result<unit,unit>Requires an already-computed condition where there is no subject value to preserve. The condition is already computed and stands alone, so success produces Ok (). Use okIf/failIf instead when the value under test should flow through. request.AcceptedTerms |> Result.require |> Result.orError TermsNotAccepted
orError: 'error -> Result<'value,'discardedError> -> Result<'value,'error>Replaces whatever error a result carries with the supplied typed error. Ok passes through unchanged. The natural follow-up to okIf/failIf, which fail with unit precisely so the reason is chosen here: value |> Result.okIf isValid |> Result.orError MyError. Use Result.mapError instead when the existing error carries something worth keeping, as a Violation does.
Requires & types
Requires: open Reified; open Reified.ConstraintDSL; open Reified.Result; open Reified.ResultDSL (packages: Reified.Constraint, Reified.Parse, Reified.Result)val parseTier: string -> Result<Tier,TicketError>val parseTicketRequest: string -> string -> Result<(Tier * int),TicketError>Construct domain values
Parsing and refinement stay separate steps: Parse.int turns text into an int, and the refinement decides whether
that int is an AttendeeId.
type AttendeeId =
private
| AttendeeId of int
member this.Value =
let (AttendeeId value) = this
value
module AttendeeId =
let refinement = Refinement.define (Constraint.greaterThan 0) AttendeeId _.Value
let create value = Refinement.create refinement value
type ContactEmail = ContactEmail of NonBlankString
type ContactError = InvalidId | InvalidEmail
type Contact = { Id: AttendeeId; Email: ContactEmail }
let createContact (rawId: string) (rawEmail: string) : Result<Contact, ContactError> =
result {
let! parsedId = Parse.int rawId |> Result.mapError (fun _ -> InvalidId)
let! id = AttendeeId.create parsedId |> Result.mapError (fun _ -> InvalidId)
let! email = Refine.nonBlankString rawEmail |> Result.mapError (fun _ -> InvalidEmail)
return { Id = id; Email = ContactEmail email }
}
FsLiveDocsGeneratedPage12_163BCE492C58.AttendeeIdAttendeeIdintAn abbreviation for the CLI type . Basic Types
this: AttendeeIdValue: AttendeeId -> unit -> intvalue: intFsLiveDocsGeneratedPage12_163BCE492C58.AttendeeIdModulerefinement: Refinement<int,AttendeeId>Reified.Refinements.RefinementCreates and applies reusable refinement definitions.
define: Constraint<'underlying> -> ('underlying -> 'refined) -> ('refined -> 'underlying) -> Refinement<'underlying,'refined>Defines a refinement from one constraint, a constructor, and the reverse projection. Compose several rules with Constraint.all before defining, and reach for Constraint.custom when the rule is an arbitrary predicate. Both produce an ordinary constraint, so there is no separate plural or check-taking constructor. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value)
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
greaterThan: 'value -> Constraint<'value>Requires a value strictly greater than the supplied bound. let quantity : Constraint<int> = Constraint.greaterThan 0
_arg1: AttendeeIdValue: intcreate: int -> Result<AttendeeId,Violation>create: Refinement<'underlying,'refined> -> 'underlying -> Result<'refined,Violation>Constructs a refined value, reporting why the raw value was not admitted. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value) 3 |> Refinement.create retryCount |> Result.mapError Violation.render
FsLiveDocsGeneratedPage12_163BCE492C58.ContactEmailContactEmailReified.Refinements.NonBlankStringA string that is not null, empty, or whitespace.
FsLiveDocsGeneratedPage12_163BCE492C58.ContactErrorInvalidIdInvalidEmailFsLiveDocsGeneratedPage12_163BCE492C58.ContactId: AttendeeIdEmail: ContactEmailcreateContact: string -> string -> Result<Contact,ContactError>rawId: stringstringAn abbreviation for the CLI type . Basic Types
rawEmail: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
result: ResultBuilderThe fail-fast result { } computation expression.
parsedId: intReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
(|>): '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.Result.ResultModuleFail-fast helpers over the standard F# Result type.
mapError: ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>Maps the error value of a result.
id: AttendeeIdemail: NonBlankStringReified.Refinements.RefineModuleSmart constructors for built-in refined values, and the refinements behind them. Text, Character, Collection, and Choice are nested here rather than declared beside Refine. Each names a concept general enough to shadow something a caller already has in scope, and none of them is a companion to a type this package exports, so there is nothing to gain from the type-and-module pairing that keeps NonBlankString and DistinctList at the top level.
nonBlankString: string -> Result<NonBlankString,Violation>Requires & types
Requires: open Reified; open Reified.Refinements; open Reified.Result; open Reified.ResultDSL (packages: Reified.Constraint, Reified.Parse, Reified.Refinements, Reified.Result)val refinement: Refinement<int,AttendeeId>val create: int -> Result<AttendeeId,Violation>val createContact: string -> string -> Result<Contact,ContactError>AttendeeId.create already returns an AttendeeId, and Refine.nonBlankString already returns a NonBlankString, so
the record takes them as they are. Nothing is checked or wrapped twice.
Positive integers are a constraint rather than a shipped type: F# cannot carry "greater than zero" through arithmetic,
so a built-in PositiveInt would cost more at every use site than it saves. Define one over the constraint, as above,
when your domain wants the name.
The full reference app adds Schema for structured input, path-aware diagnostics, codecs, and contracts. Effectful application work sits outside Reified, in the Axial repository.