Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonDefine Refined Types
A refined type is a private wrapper, a checked constructor, and one canonical way to recover the underlying value. This page is the reference for that machinery.
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
ReifiedRefinements06-refined_40-domain-values.md_page.ContactEmailContactEmailstringAn abbreviation for the CLI type . Basic Types
06-refined_40-domain-values.md_page.ContactEmailModulevalue: ContactEmail -> string_arg1: ContactEmailvalue: 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.
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.define
Constraint.email
ContactEmail
value
let create raw =
Refinement.create refinement rawContactEmail.create is now the only way in. Construction returns check failures directly:
let email : Result<ContactEmail, Violation> =
ContactEmail.create rawEmailCombine constraints
defineAll requires at least one constraint and checks every constraint against the same original value:
module ContactEmail =
let value (ContactEmail value) = value
let refinement =
Refinement.defineAll
[ Constraint.present
Constraint.email
Constraint.maxLength 254 ]
ContactEmail
value
let create raw = Refinement.create refinement rawThe same constraint values provide executable checks and ConstraintDetails metadata.
Use a metadata-free check
Use defineWithCheck for an invariant no built-in constraint describes. The check runs, but nothing downstream can
read the rule — the trade-off is
interpreted versus opaque:
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.Custom "even" ]
let refinement =
Refinement.defineWithCheck even EvenInt valueProjection law
For every successful construction, the reverse projection returns the supplied underlying value:
let result =
ContactEmail.create "ada@example.com"
|> Result.map (Refinement.underlying ContactEmail.refinement)
// Ok "ada@example.com"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:
module ContactEmail =
// ... as above
/// Total: lower-casing inhabited, well-formed text leaves it inhabited and well-formed.
let normalise (input: ContactEmail) = ContactEmail(value input |> fun text -> text.ToLowerInvariant())If you cannot write an operation like that, the concept is probably a constraint rather than a type.
Continue with Schema Integration.