Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultDefine Refined Types
A refined type is a private wrapper, a checked constructor, and one canonical way to recover the underlying value. Before reaching for it, decide whether the concept deserves a type at all. Checked construction is how a value is admitted, not a reason on its own: if nothing downstream becomes total or loses a branch, the rule belongs in a constraint on the primitive instead. Numeric ranges are the clearest example, F# cannot carry "greater than zero" through arithmetic, so a refined number costs more at every use site than it saves. When not to make a type draws the line, and Customer Id works a full example through.
Define the wrapper and Value projection
open Reified
open Reified.Refinements
type ContactEmail =
private
| ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
ReifiedRefinementsFsLiveDocsGeneratedPage25_163BCE492C58.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage25_163BCE492C58.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: stringRequires & types
val value: ContactEmail -> stringGuard construction with a refinement
Use Refinement.define when one constraint from the built-in catalogue describes admission. Those constraints are
interpreted: Reified can read the rule as data, so the refinement
carries metadata other tools can use, not only a check.
open Reified
open Reified.Refinements
type ContactEmail =
private
| ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.define
Constraint.email
ContactEmail
value
let create raw =
Refinement.create refinement raw
ReifiedRefinementsFsLiveDocsGeneratedPage26_163BCE492C58.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage26_163BCE492C58.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: stringrefinement: Refinement<string,ContactEmail>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.
email: Constraint<string>Requires text to match Reified's pragmatic email shape, ^[^@]+@[^@]+$. let contact : Constraint<string> = Constraint.email
create: string -> Result<ContactEmail,Violation>raw: stringcreate: 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
ContactEmail.create is now the only way in. Construction returns check failures directly:
let email : Result<ContactEmail, Violation> =
ContactEmail.create rawEmailCombine constraints
Constraint.all checks every listed constraint against the same original value and combines their failures; give
the combined constraint to the same Refinement.define used above:
open Reified
open Reified.Refinements
type ContactEmail =
private
| ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.define
(Constraint.all
[ Constraint.present
Constraint.email
Constraint.maxLength 254 ])
ContactEmail
value
let create raw = Refinement.create refinement raw
ReifiedRefinementsFsLiveDocsGeneratedPage27_163BCE492C58.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage27_163BCE492C58.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: stringrefinement: Refinement<string,ContactEmail>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.
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
email: Constraint<string>Requires text to match Reified's pragmatic email shape, ^[^@]+@[^@]+$. let contact : Constraint<string> = Constraint.email
maxLength: int -> Constraint<^value>Requires text or a collection to have at most the supplied size. let summary : Constraint<string> = Constraint.maxLength 280
create: string -> Result<ContactEmail,Violation>raw: stringcreate: 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
The same constraint values provide executable checks and ConstraintDescription metadata.
Use a metadata-free check
Use Constraint.customWith for an invariant no built-in constraint describes, then hand that opaque constraint to
Refinement.define like any other. The check runs, but nothing downstream can read the rule, the trade-off is
interpreted versus opaque. customWith's metadata label ("even-number rule" below) only
feeds that opaque ConstraintDescription; a caller sees the callback's own Described text on failure, not that
string:
open Reified
open Reified.Refinements
type EvenInt = private EvenInt of int
module EvenInt =
let value (EvenInt value) = value
let private even value =
if value % 2 = 0 then Ok ()
else Error(Violation.described "must be an even number")
let refinement =
Refinement.define (Constraint.customWith "even-number rule" even) EvenInt value
ReifiedRefinementsFsLiveDocsGeneratedPage28_163BCE492C58.EvenIntEvenIntintAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage28_163BCE492C58.EvenIntModulevalue: EvenInt -> int_arg1: EvenIntvalue: inteven: int -> Result<unit,Violation>(%): ^T1 -> ^T2 -> ^T3Overloaded modulo operator The first parameter. The second parameter. The result of the operation. 29 % 5 // Evaluates to 4
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
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.
Reified.ViolationModuleInspects, traverses, and renders violations.
described: string -> ViolationReports an opaque failure with author-supplied prose and no catalogue key. Violation.described "must be even" // Atomic (Described ("must be even", None))
refinement: Refinement<int,EvenInt>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.
customWith: string -> ('value -> Result<unit,Violation>) -> Constraint<'value>Runs an arbitrary callback that reports its own violation. Use this when the failure deserves a structured reason a bare predicate cannot give. Because the callback supplies only a violation-returning function, test runs it and discards the error, so a failing test costs whatever the callback allocates. Returning an Expected leaf makes no false portable claim: the enclosing description is still opaque. The callback's shape is exactly Constraint.check applied to a constraint, so the usual way to supply a structured reason is to reuse a built-in rather than build a violation by hand. labels the opaque description; the callback's violation supplies the user-facing failure message. let supported = [ "USD"; "EUR" ] in Constraint.customWith "supported currency rule" (Constraint.check (Constraint.oneOf supported))
Projection law
For every successful construction, the reverse projection returns the supplied underlying value:
open Reified
open Reified.Refinements
type ContactEmail =
private
| ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.define
(Constraint.all [ Constraint.present; Constraint.email; Constraint.maxLength 254 ])
ContactEmail
value
let create raw = Refinement.create refinement raw
let result =
ContactEmail.create "ada@example.com"
|> Result.map (Refinement.underlying ContactEmail.refinement)
// Ok "ada@example.com"
ReifiedRefinementsFsLiveDocsGeneratedPage29_163BCE492C58.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage29_163BCE492C58.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: stringrefinement: Refinement<string,ContactEmail>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.
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
email: Constraint<string>Requires text to match Reified's pragmatic email shape, ^[^@]+@[^@]+$. let contact : Constraint<string> = Constraint.email
maxLength: int -> Constraint<^value>Requires text or a collection to have at most the supplied size. let summary : Constraint<string> = Constraint.maxLength 280
create: string -> Result<ContactEmail,Violation>raw: stringcreate: 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
result: Result<string,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.Result.ResultModuleFail-fast helpers over the standard F# Result type.
map: ('a -> 'b) -> Result<'a,'c> -> Result<'b,'c>Maps the success value of a result.
underlying: Refinement<'underlying,'refined> -> 'refined -> 'underlyingReturns the canonical underlying representation of a refined value. type RetryCount = RetryCount of int let retryCount = Refinement.define (Constraint.between 0 10) RetryCount (fun (RetryCount value) -> value) RetryCount 3 |> Refinement.underlying retryCount // 3
Choose one concrete underlying representation. For collection refinements, prefer 'a list or 'a array rather than
an arbitrary seq<'a>.
Give the type its operations
A wrapper that only checks on the way in leaves callers unwrapping it at first use. What makes the type worth having is the family of operations that preserve its invariant, so the fact stays true without being rechecked:
open Reified
open Reified.Refinements
type ContactEmail =
private
| ContactEmail of string
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.define
(Constraint.all [ Constraint.present; Constraint.email; Constraint.maxLength 254 ])
ContactEmail
value
let create raw = Refinement.create refinement raw
// ... as in "Combine constraints" above
/// Total for this refinement: lower-casing never introduces an `@`, so the `email`
/// shape survives, and it does not lengthen the text, so `maxLength 254` still holds.
/// The proof is specific to the constraints in `refinement` — change them and revisit.
let normalise (input: ContactEmail) = ContactEmail(value input |> fun text -> text.ToLowerInvariant())
ReifiedRefinementsFsLiveDocsGeneratedPage30_163BCE492C58.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage30_163BCE492C58.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: stringrefinement: Refinement<string,ContactEmail>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.
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
email: Constraint<string>Requires text to match Reified's pragmatic email shape, ^[^@]+@[^@]+$. let contact : Constraint<string> = Constraint.email
maxLength: int -> Constraint<^value>Requires text or a collection to have at most the supplied size. let summary : Constraint<string> = Constraint.maxLength 280
create: string -> Result<ContactEmail,Violation>raw: stringcreate: 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
normalise: ContactEmail -> ContactEmailinput: ContactEmail(|>): '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
text: stringToLowerInvariant: unit -> stringLower-casing is closed under this refinement because the email constraint only tests that
the text is two non-empty runs of non-@ characters around one @, and lower-casing never
introduces an @, while maxLength 254 survives because ToLowerInvariant keeps the
string length. That argument is tied to the rules listed in refinement; if the
constraints change to something case-sensitive or length-critical, normalise has to be
revisited.
If no operation preserves the invariant, use a constraint instead.
Continue with Schema Integration.