Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultCustomer Id Tutorial
The built-in types cover common shapes. This tutorial defines a domain-specific type and tests whether it warrants a wrapper.
open Reified
open Reified.Refinements
ReifiedRefinementsDecide whether it earns a type
A refined type is worth defining when the invariant does something for the code that receives it. Ask what becomes total, or what branch disappears:
| Candidate | Verdict |
|---|---|
CustomerId, a positive account number |
Type. Lookup, ordering, and equality all rely on it, and an id of 0 is a bug you want to catch once. |
EmailAddress, matches an email pattern |
Constraint. Nothing downstream is total because of it; you unwrap it to send mail. |
ShippingWeight, positive, and summed across a parcel |
Constraint. The moment you add two of them, F# cannot carry "positive" through, so the type turns every sum into a Result. |
NormalisedName, trimmed and lower-cased |
Neither. That is a transformation, so it belongs in Parse. |
Only the first changes what later code can assume. The second is real validation with no downstream consequence; the third is validation that arithmetic immediately undoes. Both stay constraints on the underlying value:
field _.Email {
constrain Constraint.present
constrain Constraint.email
}Define the type
A refined type is a private wrapper, one canonical projection, and a refinement:
type CustomerId =
private
| CustomerId of int
/// The canonical underlying representation.
member this.Value =
let (CustomerId value) = this
value
override this.ToString() =
string this.Value
module CustomerId =
/// Admission and its reverse projection, packaged together.
let refinement =
Refinement.define
(Constraint.greaterThan 0) // the rule over the underlying int
CustomerId // wrap a value that passed
_.Value // unwrap again, always
let create value = Refinement.create refinement value
let value (input: CustomerId) = input.Value
FsLiveDocsGeneratedPage37_6BB268B4E740.CustomerIdCustomerIdintAn abbreviation for the CLI type . Basic Types
this: CustomerIdValue: CustomerId -> unit -> intvalue: intToString: CustomerId -> unit -> stringstring: 'T -> stringConverts the argument to a string using ToString. For standard integer and floating point values and any type that implements IFormattableToString conversion uses CultureInfo.InvariantCulture. The input value. The converted string. string 'A' // evaluates to "A" string 0xff // evaluates to "255" string -10 // evaluates to "-10"
Value: intFsLiveDocsGeneratedPage37_6BB268B4E740.CustomerIdModulerefinement: Refinement<int,CustomerId>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: CustomerIdcreate: int -> Result<CustomerId,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
value: CustomerId -> intinput: CustomerIdThe case is private, so CustomerId.create is the only way in:
CustomerId.create 42 // Ok
CustomerId.create 0 // Error (Atomic (Expected (RelationAtom (Compared (GreaterThan, Integer 0L)), Some (Integer 0L))))
FsLiveDocsGeneratedPage37_6BB268B4E740.CustomerIdModulecreate: int -> Result<CustomerId,Violation>Use Constraint.all [ ... ] when several constraints describe admission, or
Constraint.customWith for an invariant no built-in constraint describes, then hand either to the same
Refinement.define. See Define Refined Types for both.
Give it the operations that justify it
CustomerId is a key; its operations support lookup and identity, not arithmetic:
open Reified
open Reified.Refinements
type CustomerId =
private
| CustomerId of int
member this.Value =
let (CustomerId value) = this
value
module CustomerId =
let refinement = Refinement.define (Constraint.greaterThan 0) CustomerId _.Value
let create value = Refinement.create refinement value
let value (input: CustomerId) = input.Value
// ... as above
/// Total: distinct ids stay distinct, so no entry can be lost.
let index (customers: DistinctList<CustomerId>) = DistinctList.toSet customers
/// Total: ids are ordered, so a range of them is an interval.
let range (first: CustomerId) (second: CustomerId) = Interval.between first second
ReifiedRefinementsFsLiveDocsGeneratedPage38_6BB268B4E740.CustomerIdCustomerIdintAn abbreviation for the CLI type . Basic Types
this: CustomerIdValue: CustomerId -> unit -> intvalue: intFsLiveDocsGeneratedPage38_6BB268B4E740.CustomerIdModulerefinement: Refinement<int,CustomerId>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: CustomerIdValue: intcreate: int -> Result<CustomerId,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
value: CustomerId -> intinput: CustomerIdindex: DistinctList<CustomerId> -> Set<CustomerId>customers: DistinctList<CustomerId>Reified.Refinements.DistinctList`1A list with no duplicate items, preserving first-seen order.
Reified.Refinements.DistinctListModuleOperations over lists known to hold no duplicates.
toSet: DistinctList<'value> -> Set<'value>Builds a set. Total and lossless — this is the operation that justifies the type, because distinct items always produce a set of the same size, while Set.ofList on an ordinary list silently collapses duplicates.
range: CustomerId -> CustomerId -> Interval<CustomerId>first: CustomerIdsecond: CustomerIdReified.Refinements.IntervalModuleOperations over inclusive ranges of ordered values.
between: 'a -> 'a -> Interval<'a>Builds the smallest interval containing both values, ordering them as needed. Total — this is the constructor to reach for first. Requires a total order. With float or float32, a NaN argument cannot be ordered against anything and produces inverted bounds; prefer Interval<FiniteFloat> or create there.
Both work because the invariant is a fact about the value, not about the moment of construction, and neither involves arithmetic, which is where F# stops being able to carry the invariant for you. If no such operation applies, use a constraint instead.
Use it in domain code
let loadCustomer (id: CustomerId) =
// No guard: id is known to be above zero.
repository.load id.ValueKeep the raw type at input and storage boundaries, and the refined type in between.
Give it a schema
Refinement.constraints exposes the same rule to Schema and other interpreters, so a
boundary describes the type without restating it:
open Reified
let customerIdSchema : Schema<CustomerId> =
Schema.int |> Schema.refine CustomerId.refinement
ReifiedcustomerIdSchema: Schema<CustomerId>Reified.Schema`1Describes a typed value's portable structure and construction for schema interpreters. A schema records shape and construction metadata without tying that metadata to input parsing, diagnostics, validation, codecs, UI generation, or workflow execution. Primitive, collection, optional, union, refined, and record declarations all produce Schema<'value>. Record declarations use the schema<'value> { } computation expression. Each field may contain withSchema, constrain, constraints, refine, and validate operations before the declaration finishes with construct or constructResult.
FsLiveDocsGeneratedPage37_6BB268B4E740.CustomerIdReified.SchemaConstruction, composition, parsing, and checking for universal schemas.
int: Schema<int>Describes 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
refine: Refinement<'raw,'value> -> Schema<'raw> -> Schema<'value>Maps a raw schema through a reusable bidirectional refinement. The smart constructor runs during parsing. Inspection supplies the raw representation during checking and encoding.
FsLiveDocsGeneratedPage37_6BB268B4E740.CustomerIdModulerefinement: Refinement<int,CustomerId>Parsing checks the int, constructs the CustomerId, and reports failures at the field's
path. Encoding projects back through Value. The emitted JSON Schema carries
exclusiveMinimum: 0 because the constraint travelled with the refinement, you did not
write the rule twice.
To resolve the type in a bare field without a withSchema, register it once as described
in Refined Schemas, which works the same
example through the Schema DSL and shows where schema-local constraints fit alongside it.
Next
- Order Totals, the built-in types used in anger.
- Define Refined Types, the full
Refinementreference. - Schema Integration, applying refinements at structured boundaries.
- Refined Schemas, the Schema-side view.