Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonIntroductory 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
RefinementsReusable 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 print on one line: 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)
04-constraints_60-reference-app.md_page.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
Dependent work
result { } sequences steps that depend on each other, so the first failure stops the pipeline:
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
}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
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 }
}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.