Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultConstraint
When a check and its message are defined separately, changing a length limit can leave the error quoting the old one.
A Constraint<'value> describes valid values and derives failures from the same description. Constraint.check
runs it.
open Reified
let retryCount : Constraint<int> =
Constraint.between 0 10
3 |> Constraint.satisfies retryCount // true
42
|> Constraint.check retryCount
|> Result.mapError Violation.render
// Error "expected a value between 0 and 10, but was 42"
3 |> Constraint.guard retryCount // Ok 3
ReifiedretryCount: Constraint<int>Reified.Constraint`1A reusable description of valid values, coupled to the closures that execute it. One constraint value serves direct checking, refined-value admission, Schema, documentation, and export. There is no separate check type: check is the operation, Constraint is the noun. Both closures are retained deliberately. They are not duplicates of one rule: test over a conjunction may stop at the first failing child, while check must run every child to accumulate. Interpreted atoms and custom predicates therefore have a Boolean path that does no violation work, and combinators preserve that property when every child has it. A customWith constraint supplies only a violation-returning callback, so its test runs that callback and discards the error. The description is never interpreted during execution. Closures are composed once, at construction.
intAn abbreviation for the CLI type . Basic Types
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
between: 'value -> 'value -> Constraint<'value>Requires a value inside the supplied inclusive bounds. let retryCount : Constraint<int> = Constraint.between 0 10
(|>): '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
satisfies: Constraint<'value> -> 'value -> boolAnswers whether a value satisfies a constraint, without building a violation. let retryCount = Constraint.between 0 10 3 |> Constraint.satisfies retryCount // true
check: Constraint<'value> -> 'value -> Result<unit,Violation>Runs a constraint, returning why the value failed. let retryCount = Constraint.between 0 10 42 |> Constraint.check retryCount |> Result.mapError Violation.render
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.
Reified.ViolationModuleInspects, traverses, and renders violations.
render: Violation -> stringRenders a violation as an English sentence fragment with no trailing punctuation, keeping conjunction and alternative groups distinct. Violation.render (Atomic (Expected (PresenceAtom Present, None))) // "value must be present"
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
Change the bounds and the derived message changes with them.
When you only want your own error
When a function needs only the application's error case, Constraint.guard keeps the checked value and
Result.orError discards the violation:
let validateEmail raw : Result<string, SignupError> =
raw
|> Constraint.guard Constraint.email
|> Result.orError InvalidEmailYou can reuse Constraint.email in a refinement, a schema, or a JSON Schema document without rewriting the rule.
Result.orError comes from Reified.Result. Without that package, Result.mapError (fun _ -> InvalidEmail) does the
same thing.
Where the rules for a model live
Once there is more than one rule, they belong together in a module rather than scattered across the code that
uses them. That is also where Reified.ConstraintDSL earns its place: it exposes the same constructors
without the Constraint. prefix, which reads well when the module name already says what these rules are for.
open Reified
module SignupRules =
open Reified.ConstraintDSL
let emailAddress : Constraint<string> = Constraint.all [ present; email; maxLength 254 ]
let age : Constraint<int> = atLeast 13
ReifiedFsLiveDocsGeneratedPage7_6BB268B4E740.SignupRulesReified.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
emailAddress: Constraint<string>Reified.Constraint`1A reusable description of valid values, coupled to the closures that execute it. One constraint value serves direct checking, refined-value admission, Schema, documentation, and export. There is no separate check type: check is the operation, Constraint is the noun. Both closures are retained deliberately. They are not duplicates of one rule: test over a conjunction may stop at the first failing child, while check must run every child to accumulate. Interpreted atoms and custom predicates therefore have a Boolean path that does no violation work, and combinators preserve that property when every child has it. A customWith constraint supplies only a violation-returning callback, so its test runs that callback and discards the error. The description is never interpreted during execution. Closures are composed once, at construction.
stringAn abbreviation for the CLI type . Basic Types
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
all: Constraint<'value> list -> Constraint<'value>Requires every constraint to hold, evaluating each in declaration order and accumulating failures. The empty list is the satisfied identity. F# visits list elements left to right, so annotate the binding when the first element is a type-directed value: let requiredName : Constraint<string> = Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]. let requiredName : Constraint<string> = Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]
present: Constraint<^value>Alias for .
email: Constraint<string>Alias for .
maxLength: int -> Constraint<^value>Alias for .
age: Constraint<int>intAn abbreviation for the CLI type . Basic Types
atLeast: 'a -> Constraint<'a>Alias for .
Open it inside the module, not at the top of a file, present and email are ordinary words, and their
meaning should be obvious from the two lines above them. The DSL changes vocabulary, not semantics: every name
returns the same Constraint<'value> the qualified name returns. Code elsewhere refers to SignupRules.age,
and the rest of this page uses Constraint. spellings so each example stands on its own.
Reach for the structured path when the extra facts earn their keep, when you want to classify failures, render
messages in more than one language, or report several field failures at once. Then keep the violation and
Result.mapError it into a case that carries it:
raw
|> Constraint.guard Constraint.email
|> Result.mapError InvalidEmail // InvalidEmail of ViolationThe failure carries facts, not prose
A Violation holds the failing constraint atom and, where Reified can represent it, the actual value. It carries no
language and no formatting. That is what keeps it comparable data you can retain, assert on in a test, and
pass across a boundary without dragging a culture or a ResourceManager along with it.
open Reified
match "ab" |> Constraint.check (Constraint.minLength 3: Constraint<string>) with
| Ok () -> ()
| Error violation ->
printfn "%A" (Violation.tryExpectation violation)
// Some (CardinalityAtom (Cardinality.Minimum 3))
printfn "%A" (Violation.tryActual violation)
// Some (ConstraintValue.Integer 2L)
Reified(|>): '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.
check: Constraint<'value> -> 'value -> Result<unit,Violation>Runs a constraint, returning why the value failed. let retryCount = Constraint.between 0 10 42 |> Constraint.check retryCount |> Result.mapError Violation.render
minLength: int -> Constraint<^value>Requires text or a collection to have at least the supplied size. Literal size, so a single space satisfies minLength 1. Use present to reject whitespace. let tags : Constraint<string list> = Constraint.minLength 1
Reified.Constraint`1A reusable description of valid values, coupled to the closures that execute it. One constraint value serves direct checking, refined-value admission, Schema, documentation, and export. There is no separate check type: check is the operation, Constraint is the noun. Both closures are retained deliberately. They are not duplicates of one rule: test over a conjunction may stop at the first failing child, while check must run every child to accumulate. Interpreted atoms and custom predicates therefore have a Boolean path that does no violation work, and combinators preserve that property when every child has it. A customWith constraint supplies only a violation-returning callback, so its test runs that callback and discards the error. The description is never interpreted during execution. Closures are composed once, at construction.
stringAn abbreviation for the CLI type . Basic Types
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.
violation: Violationprintfn: 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.
Reified.ViolationModuleInspects, traverses, and renders violations.
tryExpectation: Violation -> ConstraintAtom optionThe failing constraint's identity, when the violation is a single interpreted leaf.
tryActual: Violation -> ConstraintValue optionThe value that failed, when the violation is a single leaf carrying a portable one.
Application code can classify a failure without parsing a sentence, and a test can assert on the fact rather than on the wording.
One language, or many, from the same violation
Prose happens at the rendering edge. Violation.render is the zero-dependency English default and needs no
setup at all. When you need more, a Renderer carries the language and the document context while the
violation carries the facts:
open Reified
let renderer = Renderer.english
let violation =
match "" |> Constraint.check Constraint.present with
| Error violation -> violation
| Ok () -> failwith "unreachable"
let field = renderer |> Renderer.context "signup" |> Renderer.attribute "name"
violation |> Violation.message field // "must be present"
violation |> Violation.fullMessage field // "Name must be present"
Reifiedrenderer: RendererReified.RendererModuleBuilds renderers and renders messages through them.
english: RendererA renderer that uses each catalogue's neutral English, with no resources at all. The default for tests, tools, and applications that never translate. It is not the same as Violation.render: this produces bare predicates that compose, while render keeps the legacy self-contained English exactly. violation |> Violation.fullMessage Renderer.english // "value must be present"
violation: Violation(|>): '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.
check: Constraint<'value> -> 'value -> Result<unit,Violation>Runs a constraint, returning why the value failed. let retryCount = Constraint.between 0 10 42 |> Constraint.check retryCount |> Result.mapError Violation.render
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
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.
failwith: string -> 'TThrow a exception. The exception message. Never returns. let failingFunction() = failwith "Oh no" // Throws an exception true // Never reaches this failingFunction() // Throws a System.Exception
field: Renderercontext: string -> Renderer -> RendererAppends a document, model, form, or component segment. let signup = renderer |> Renderer.context "signup"
attribute: string -> Renderer -> RendererReplaces the attribute with one segment. Replacement, not append: a form-scoped renderer stays reusable for sibling fields. Schema supplies its typed path through Renderer.Advanced.attributePath instead. signup |> Renderer.attribute "name"
Reified.ViolationModuleInspects, traverses, and renders violations.
message: Renderer -> Violation -> stringRenders a violation as a localized predicate, with no attribute noun. Use this where a label already names the field — a form row, or a Schema result whose returned path identifies it. Conjunctions and alternatives join through the contextual constraint.group.* patterns; an actual value, when the violation carries one, is composed in through constraint.actual. violation |> Violation.message (renderer |> Renderer.context "signup" |> Renderer.attribute "name") // "must be at least 13, but was 11"
fullMessage: Renderer -> Violation -> stringRenders a violation as a complete sentence fragment, with the attribute noun composed once. The noun wraps the finished message, never each leaf: a group of three failures still names the field once. With no attribute the contextual constraint.attribute.default supplies the noun, so an unattributed violation reads "Value must be present" rather than borrowing the document context. violation |> Violation.fullMessage (signup |> Renderer.attribute "name") // "Name must be present"
Give the renderer a different culture and the identical violation reads "Le nom doit être renseigné",
with contextual fallback, and without any application code walking a violation tree or reproducing Reified's
key catalogue.
If you ship in one language, you do not need translation resources.
The same declaration is read by everything downstream
A rule named in a module works unchanged in a refinement and in a schema. ConstraintDSL covers the full vocabulary, the type-directed names, and the result adapters; keep one small rule module here and reach for that page for details.
The same value facts appear at three further levels:
- Refined values use a constraint to construct invariant-carrying domain types.
- Schema adds structured input, paths, accumulation, and wire interpreters.
- JSON Schema publishes what the target can enforce, and documents the rest.
Where to go next
For a comparison with DataAnnotations, FluentValidation, and Validus, read How it compares.
Otherwise take ConstraintDSL for the full vocabulary and how to write a rule module, then Using constraints for composition and keeping the input, Working with violations for rendering and inspecting failures, Interpreted and opaque for what makes a rule inspectable and what an escape hatch costs, and Localization for translating failures, with Adding a language for the working order of a new translation and Fable support for the JavaScript target.