Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonUsing constraints
open Reified
ReifiedA constraint tests an existing typed value. It never trims, normalizes, or replaces its input.
Name a reusable rule
let name : Constraint<string> =
Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]
Constraint.check name "Ada"
// Ok ()
name: 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>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
lengthBetween: int -> int -> Constraint<^value>Requires a text or collection size inside the supplied inclusive bounds. let name : Constraint<string> = Constraint.lengthBetween 2 40
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
Constraint.present and the size family resolve across text, lists, arrays, and maps by the type they are used at.
Applied where that type is already known — inside a rule like the one above, or to a schema — they need nothing
extra. On a standalone binding the annotation is the only type information available, so it is what tells the
compiler which shape you meant.
Compose
Constraint.all runs every child against the same value in declaration order and accumulates the failures. The empty
list is the satisfied identity.
Constraint.any takes a first alternative plus the rest, evaluates left to right, and stops at the first success. It
never throws, because an empty disjunction — which nothing could satisfy and which has no reason to report — cannot be
written.
Use any for a valid set with a hole in it, which neither a list of literals nor a range can express. The recurring
case is a sentinel beside a real value:
/// A TTL is either the sentinel -1, meaning "never expire", or a positive number of seconds.
let ttl : Constraint<int> =
Constraint.any (Constraint.equalTo -1) [ Constraint.positive ]
ttl: 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.
any: Constraint<'value> -> Constraint<'value> list -> Constraint<'value>Requires at least one alternative to hold, evaluating left to right and stopping at the first success. When none succeeds, every rejected branch is reported. Taking the first branch separately keeps an unsatisfiable empty disjunction unrepresentable, so this never throws. Alternatives among rules are what neither oneOf (alternatives among literals) nor a range (one contiguous region) can express — a valid set with a hole in it, such as a wire value that is either a sentinel or a duration. let ttl : Constraint<int> = Constraint.any (Constraint.equalTo -1) [ Constraint.atLeast 1 ]
equalTo: 'value -> Constraint<'value>Requires equality with the supplied value, under F# structural equality. let mustBeDraft : Constraint<Status> = Constraint.equalTo Status.Draft
positive: Constraint<^value>Requires a value strictly greater than zero. let quantity : Constraint<int> = Constraint.positive
That is a wire-tier rule. Domain code should still model the union honestly as Never | After of Duration; any
exists because a fixed protocol encodes that union as one field, and the schema has to admit the encoding before a
constructor can produce the domain value.
Presence, blankness, and optionality
Three operations read as more similar than they are:
| Operation | Absence | Presence |
|---|---|---|
Constraint.present |
rejected | required to satisfy the inner shape |
Constraint.blank |
required | rejected |
Constraint.optional inner |
permitted | must satisfy inner |
So blank requires absence while optional permits it. Reaching for blank to mean "this field may be empty"
gives a constraint that rejects every real value.
let nickname : Constraint<string option> =
Constraint.optional (Constraint.lengthBetween 2 40)
nickname: Constraint<string option>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
optionThe type of optional values. When used from other CLI languages the empty option is the null value. Use the constructors Some and None to create values of this type. Use the values in the Option module to manipulate values of this type, or pattern match against the values directly. 'None' values will appear as the value null to other CLI languages. Instance methods on this type will appear as static methods to other CLI languages due to the use of null as a value representation. Options
Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
optional: Constraint<'value> -> Constraint<^container>Lifts a constraint over an optional container: absence passes, presence runs the inner constraint. Orthogonal to present and blank, which respectively require inhabitation and require absence. This one permits absence. let nickname : Constraint<string option> = Constraint.optional (Constraint.lengthBetween 2 40)
lengthBetween: int -> int -> Constraint<^value>Requires a text or collection size inside the supplied inclusive bounds. let name : Constraint<string> = Constraint.lengthBetween 2 40
Whether a property may be omitted from the input is a different question again, and belongs to Schema's
mustSupply/mayOmit.
What "blank" means
present means inhabited according to the shape: None, ValueNone, an empty Nullable, a null or empty
collection or map, and null or whitespace-only text are all blank. minLength 1 is a literal size, so a single
space satisfies it while present does not.
For text specifically, blank means whitespace as .NET defines it, plus U+FEFF (the byte-order mark). That last character is deliberate, and it is what lets the rule be published at all.
A JSON Schema validator decides whitespace by ECMA-262's \s, which is not quite .NET's set. The two used to
disagree in both directions, and one of those directions is genuinely harmful: where a validator treats a character
as whitespace and Reified does not, an exported schema rejects a payload the library would have accepted — and the
library never sees it to explain why. U+FEFF was the whole of that direction, since .NET Core dropped it from
Char.IsWhiteSpace while ECMA-262 keeps it. Treating it as blank removes the problem.
What remains is the harmless direction: a few characters, U+0085 among them, are blank here but ordinary to a
validator. Such a value passes the wire check and then fails at Reified with a proper diagnostic, which is what you
want anyway. Because of that, present on text exports as pattern: "\\S" and trimmed exports too, where both
were previously runtime-only.
Keep the value
let checkedName : Result<string, Violation> =
"Ada" |> Constraint.guard name
checkedName: Result<string,Violation>Microsoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
stringAn abbreviation for the CLI type . Basic Types
Reified.ViolationWhy a value failed its constraint. A diagnostic contract, not an application error union. Domain code maps a whole violation once with Result.mapError; Schema adds the path at which it occurred. Violations are plain comparable data. No closure and no constraint description is reachable from one, so structural equality holds and a violation can be retained and compared long after the constraint that produced it went out of scope. There is no promised wire format. Reified-produced groups are never empty and never unary: a single failing child is returned directly rather than wrapped. The first * rest shape encodes non-emptiness only; non-unarity is a normalization invariant.
(|>): '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
name: Constraint<string>guard returns the unchanged input after success. For a local condition that is not worth naming as a reusable
Constraint, Result.okIf/Result.failIf with Result.orError play the same role — see
Creating a Result.
Map the whole violation once at the application boundary:
type SignupError = InvalidName of Violation
let validateName value =
value
|> Constraint.guard name
|> Result.mapError InvalidName
04-constraints_20-overview.md_page.SignupErrorInvalidNameReified.ViolationWhy a value failed its constraint. A diagnostic contract, not an application error union. Domain code maps a whole violation once with Result.mapError; Schema adds the path at which it occurred. Violations are plain comparable data. No closure and no constraint description is reachable from one, so structural equality holds and a violation can be retained and compared long after the constraint that produced it went out of scope. There is no promised wire format. Reified-produced groups are never empty and never unary: a single failing child is returned directly rather than wrapped. The first * rest shape encodes non-emptiness only; non-unarity is a normalization invariant.
validateName: string -> Result<string,SignupError>value: string(|>): '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
name: Constraint<string>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.
Report a failure
Constraint.check and Constraint.guard return a structured Violation. Render it when you need an English message:
""
|> Constraint.check name
|> Result.mapError Violation.render
// Error "value must be present; expected a size between 2 and 40, but was 0"
(|>): '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
name: Constraint<string>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"
Keep the structured value when the application needs to retain, classify, or localize the failure. See Working with violations for rendering, domain error mapping, groups, and programmatic inspection.
Check or extract
Constraints preserve shape. check answers whether a value satisfies a rule, guard hands the same value back so a
pipeline can continue, and satisfies gives a bool when a local branch wants nothing structured:
"Ada" |> Constraint.check name // Result<unit, Violation>
"Ada" |> Constraint.guard name // Result<string, Violation> -- the value, unchanged
"Ada" |> Constraint.satisfies name // bool
(|>): '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
name: Constraint<string>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
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
None of them changes the type of the value. Extraction does, and stays an ordinary match or Option/Choice
conversion at the call site rather than living in Constraint or Result — folding "prove a fact" and "extract a
value" into one function would give a rule whose meaning depended on what the caller wanted back, and nothing
downstream could read it.
The split is what makes one constraint usable by many interpreters: a schema lowers a rule, a generator satisfies it, a document publishes it — and all three need the rule to be a claim about a value rather than a transformation of one.