Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultAdvanced rendering
Most applications never need this page. Read it when the default composition is not the sentence your language wants, or when your localization system, not Reified, should select and render entries.
Message composition
Localized entries are bare predicates:
constraint.presence.present = must be present
constraint.relation.atLeast = must be at least {expected}
constraint.cardinality.between = must have a size between {minimum} and {maximum}
The noun and the actual-value clause are separate entries:
constraint.attribute.default = value
constraint.actual = {message}, but was {actual}
constraint.fullMessage = {attribute} {message}
Rendering one interpreted leaf goes: resolve the predicate, wrap it in constraint.actual if the violation carries
an actual value, and, for fullMessage only, wrap that in constraint.fullMessage with the resolved noun.
Keeping them separate is what makes {actual} optional without an optional-placeholder rule, and it lets a locale
put the value or the noun somewhere English would not:
constraint.actual = reçu {actual} au lieu de « {message} »
constraint.fullMessage = {message}, {attribute}
Named interpolation
Templates use named placeholders with an optional format suffix:
constraint.number.multipleOf = must be a multiple of {divisor:N0}
{{ and }} produce literal braces. Placeholder parsing is identical on every target.
Resource defects degrade rather than throw:
- an unknown placeholder name stays literal, so a translator's typo shows up in the message instead of taking down the request;
- unmatched or malformed braces make that template unusable, and lookup continues down the fallback chain to the neutral English;
- an unknown or unsupported format suffix falls back to ordinary formatting of the value.
Exceptions raised by your lookup, resolver, or formatter callbacks propagate untouched. Those are application defects, not resource misses, and swallowing one would hide the bug in whichever locale nobody exercises.
Operand formatting
Operands are ConstraintValue. The built-in formatter uses the renderer's value culture and honours format
suffixes. Two hooks replace it:
open Reified
let renderer = Renderer.english
let format (value: ConstraintValue) (suffix: string option) : string =
match value, suffix with
| ConstraintValue.Integer value, Some "N0" -> value.ToString("N0")
| value, _ -> ConstraintValue.render value
// One uniform callback. Format suffixes are ignored.
renderer |> Renderer.withValues ConstraintValue.render
// The suffix, when you need it.
renderer
|> Renderer.Advanced.withValueFormatting (fun request -> format request.Value request.Format)
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"
format: ConstraintValue -> string option -> stringvalue: ConstraintValueReified.ConstraintValueThe closed set of operand and actual-value representations a constraint may carry across a description, diagnostic, or localization boundary. A value is admitted only when the representation is lossless in the semantics Reified's runtime diagnostics and exporters use. Semantic sorts keep their own case rather than being flattened into Text because their wire rendering happens to be textual: an instant and the string spelling it are different facts, and an interpreter that cannot tell them apart cannot decide whether wire equality substitutes for typed equality. Guid and TimeSpan are reached through typed dispatch rather than a runtime type test. Fable erases a Guid to a plain string and a TimeSpan to a number, so a boxed type test silently labels them Text and Integer there while .NET labels them correctly — the same constraint meaning two different things per platform. ConstraintValue.ofOperand resolves the overload at the call site, where the type is still known, so both platforms agree. Values outside this set are never boxed through the public surface. The constraint still executes against its private typed closure; the atom describes and fails as UnsupportedOperand instead. This is not a solver literal theory. A later proof phase declares its own numeric, date, and string sorts and adds a translation from the cases here; nothing may be added to this type for the solver alone.
suffix: string optionstringAn 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
IntegerAn integral value that fits a signed 64-bit integer.
value: int64SomeThe representation of "Value of type 'T" The input value. An option representing the value.
ToString: string -> stringReified.ConstraintValueModuleBuilds and renders portable constraint values.
render: ConstraintValue -> stringRenders a portable value for a default English message. Not a wire format.
(|>): '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
withValues: (ConstraintValue -> string) -> Renderer -> RendererReplaces all operand rendering with one callback, ignoring placeholder format suffixes. List operands still join through the contextual constraint.list.* patterns; the callback renders each item. Use Renderer.Advanced.withValueFormatting when the suffix matters. renderer |> Renderer.withValues (fun value -> ConstraintValue.render value)
withValueFormatting: (ValueFormatRequest -> string) -> Renderer -> RendererReplaces operand formatting with a callback that receives the placeholder's format suffix. renderer |> Renderer.Advanced.withValueFormatting (fun request -> myFormat request.Value request.Format)
Reified.RendererModule.AdvancedReplacing lookup, formatting, or inspecting the exact keys Reified will ask for.
request: ValueFormatRequestValue: ConstraintValueThe operand being formatted.
Format: string optionThe text after : in the placeholder, as in {divisor:N0}.
A list operand, choices on Constraint.oneOf, for instance, is joined through the contextual
constraint.list.* patterns, and each item goes through the formatter.
Lists and groups
Joining is pattern-driven, not hard-coded:
constraint.group.all.pair constraint.group.any.pair constraint.list.pair
constraint.group.all.start constraint.group.any.start constraint.list.start
constraint.group.all.middle constraint.group.any.middle constraint.list.middle
constraint.group.all.end constraint.group.any.end constraint.list.end
pair and end take {first} and {second}; start and middle take {first} and {rest}. The algorithm is
deterministic:
| Items | Result |
|---|---|
| none | "", with no lookup |
| one | the item itself, with no lookup |
| two | pair |
| three or more | combine the last two with end, fold the preceding items right-to-left with middle, and apply start to the first |
open Reified
open Reified.ConstraintDSL
let name : Constraint<string> = Constraint.all [ present; lengthBetween 2 40; email ]
let group =
match "" |> Constraint.check name with
| Error violation -> violation
| Ok () -> failwith "unreachable"
Violation.message Renderer.english group
// "must be present, must have a size between 2 and 40 and must be an email address"
ReifiedReified.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
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>Alias for .
lengthBetween: int -> int -> Constraint<^value>Alias for .
email: Constraint<string>Alias for .
group: 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
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
ErrorRepresents an Error or a Failure. The code failed with a value of 'TError representing what went wrong.
violation: ViolationOkRepresents 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
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"
Reified.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"
Requires & types
Requires: open Reified; open Reified.ConstraintDSL (packages: Reified.Constraint)val name: Constraint<string>val group: ViolationA language whose joining cannot be expressed as pair/start/middle/end, where the conjunction changes the words of
its members, or the whole group has to be reordered, is not served by these patterns. That case takes
Violation.toMessageTree and owns its own traversal. This is a genuine limit, not a gap to work around with a
cleverer pattern.
Plurals
An entry may declare at most one plural operand. Where one is declared, ordinary lookup tries <key>.one when the
operand is exactly one and <key>.other otherwise, then the bare key, at each contextual level, before moving
outwards:
signup.tags.constraint.cardinality.minimum.other
signup.tags.constraint.cardinality.minimum
signup.constraint.cardinality.minimum.other
signup.constraint.cardinality.minimum
constraint.cardinality.minimum.other
constraint.cardinality.minimum
A bare field-specific entry therefore beats a pluralized model-level entry: the field override is the more deliberate statement.
.one/.other covers the languages that need only those two. Full CLDR category selection belongs to an advanced
resolver.
Advanced resolvers
Renderer.ofLookup owns the entire candidate order and asks a plain lookup for exact keys. When your localization
system wants to select plural categories and render entries itself, ICU, for instance, take a resolver instead.
It receives one MessageRequest per contextual level. MessageRequest is the real public type from Reified; it
carries the encoded contextual BaseKey (no plural suffix applied), the interpolation Arguments, and the
PluralArgument when the owning catalogue declares one. Do not redeclare it.
A dictionary-backed resolver is a runnable starting point:
open Reified
let translations =
Map.ofList
[ "constraint.relation.atLeast", "must be at least {expected}"
"constraint.attribute.default", "value"
"constraint.actual", "{message}, but was {actual}" ]
let renderer =
Renderer.Advanced.ofResolver (fun request ->
translations.TryFind request.BaseKey
|> Option.map MessageResolution.Template)
Reifiedtranslations: Map<string,string>Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
ofList: ('Key * 'T) list -> Map<'Key,'T>Returns a new map made from the given bindings. The input list of key/value pairs. The resulting map. let input = [ (1, "a"); (2, "b") ] input |> Map.ofList // evaluates to map [(1, "a"); (2, "b")]
renderer: RendererReified.RendererModuleBuilds renderers and renders messages through them.
ofResolver: MessageResolver -> RendererA renderer backed by a resolver that answers one contextual level at a time. Use this for ICU or any system that selects plural categories and renders entries itself. Reified still owns contextual fallback and violation composition; a system that must reorder a whole group takes Violation.toMessageTree instead. Renderer.Advanced.ofResolver (fun request -> icu.TryRender(request.BaseKey, request.Arguments))
Reified.RendererModule.AdvancedReplacing lookup, formatting, or inspecting the exact keys Reified will ask for.
request: MessageRequestTryFind: string -> string optionLookup an element in the map, returning a Some value if the element is in the domain of the map and None if not. The input key. The mapped value, or None if the key is not in the map. let sample = Map [ (1, "a"); (2, "b") ] sample.TryFind 1 // evaluates to Some "a" sample.TryFind 3 // evaluates to None
BaseKey: stringThe encoded contextual resource key, without a plural suffix.
(|>): '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
Microsoft.FSharp.Core.OptionModuleContains operations for working with options. Options
map: ('T -> 'U) -> 'T option -> 'U optionmap f inp evaluates to match inp with None -> None | Some x -> Some (f x). A function to apply to the option value. The input option. An option of the input value after applying the mapping function, or None if the input is None. None |> Option.map (fun x -> x * 2) // evaluates to None Some 42 |> Option.map (fun x -> x * 2) // evaluates to Some 84
Reified.MessageResolutionWhat an advanced resolver found for one contextual level.
TemplateA template Reified should interpolate and format.
Requires & types
Requires: open Reified (packages: Reified.Constraint)val translations: Map<string,string>val renderer: RendererAn ICU-backed host resolver has the same shape; icu below is the host application's own library, not a Reified
value:
let icuRenderer =
Renderer.Advanced.ofResolver (fun request ->
match icu.TryGet request.BaseKey with
| Some entry -> Some (MessageResolution.Rendered (entry.Format request.Arguments))
| None -> None)The resolver's return value means:
None, continue to the next, less specific level;MessageResolution.Templatetemplate, Reified interpolates and formats it;MessageResolution.Renderedtext, you rendered it; Reified never interpolates that text again, so literal braces in it stay literal.
Rendered is final for the entry that was requested. Reified may still use its text as the {message} of
constraint.actual, as a child of a group pattern, or as the {message} of constraint.fullMessage, because
those are separate entries with their own resolution.
A resolver does not replace group traversal. A system that must reorder or reinterpret a whole group takes
Violation.toMessageTree.
Inspecting what will be asked for
open Reified
let renderer = Renderer.english
let spec =
MessageDescriptor.Advanced.ofSegments [ "billing"; "cardExpired" ] Map.empty
|> MessageFormatSpec.Advanced.create "card has expired" None
Renderer.Advanced.lookupCandidates renderer spec // exact encoded keys, in order
Renderer.Advanced.messageRequests renderer spec // one request per contextual level
Renderer.Advanced.attributeCandidates renderer // encoded attribute-noun keys
Renderer.Advanced.format spec renderer // render any catalogue's entry: "card has expired"
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"
spec: MessageFormatSpecReified.MessageDescriptorModuleReads and builds message identities.
ofSegments: string list -> Map<string,ConstraintValue> -> MessageDescriptorBuilds a descriptor from already-parsed segments, skipping the parse. For a generated catalogue whose segments are known at build time, so a render does not reparse a key it has already validated. Segments are unencoded and may contain any character except that an empty segment, or no segments at all, is rejected. MessageDescriptor.Advanced.ofSegments [ "schema"; "omitted" ] Map.empty
Reified.MessageDescriptorModule.AdvancedBuilding descriptors from keys that were not written by the calling programmer.
Microsoft.FSharp.Collections.MapModuleContains operations for working with values of type .
empty: Map<'Key,'T>The empty map. let emptyMap = Map.empty<int, 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.MessageFormatSpecModuleReads and builds catalogue-owned rendering metadata.
create: string -> string option -> MessageDescriptor -> MessageFormatSpecBuilds a specification, raising when the plural operand names no argument. let descriptor = MessageDescriptor.Advanced.create "constraint.relation.atLeast" Map.empty in MessageFormatSpec.Advanced.create "must be at least {expected}" None descriptor
Reified.MessageFormatSpecModule.AdvancedBuilding specifications for a catalogue of your own.
NoneThe representation of "No value"
lookupCandidates: Renderer -> MessageFormatSpec -> string listEvery encoded resource key ordinary lookup will try, in order. Includes the selected .one/.other key and the bare key at each contextual level. Use it to test translation coverage for contexts and fields Reified cannot enumerate for you. Renderer.Advanced.lookupCandidates renderer spec |> List.head
Reified.RendererModule.AdvancedReplacing lookup, formatting, or inspecting the exact keys Reified will ask for.
messageRequests: Renderer -> MessageFormatSpec -> MessageRequest listOne request per contextual level, as an advanced resolver receives them. Renderer.Advanced.messageRequests renderer spec |> List.map _.BaseKey
attributeCandidates: Renderer -> string listEvery encoded attribute-noun key, most specific first. let renderer = Renderer.ofLookup (fun _ -> None) in Renderer.Advanced.attributeCandidates renderer // [ "attribute.signup.postcode"; "attribute.postcode" ]
format: MessageFormatSpec -> Renderer -> stringRenders any catalogue's entry through the full contextual, plural, and formatting path. The entry point another package's catalogue uses. Schema renders its schema.* entries with this and adds no Schema knowledge to Reified.Constraint. renderer |> Renderer.Advanced.format spec
These take a MessageFormatSpec: a message identity and arguments (MessageDescriptor) plus the owning
catalogue's neutral fallback and plural operand. That pairing is what lets Schema, or your own catalogue, reuse
every renderer mechanic without Reified.Constraint knowing a single one of its keys.
lookupCandidates and the rest return encoded resource keys. Canonical unencoded identity comes only from
MessageDescriptor.key.
Violation.render compatibility
Violation.render remains the resource-free, culture-free path, unchanged. It does not go through Renderer.
| Violation | render |
message Renderer.english |
fullMessage Renderer.english |
|---|---|---|---|
| presence, no actual | value must be present |
must be present |
value must be present |
relation, actual 11 |
expected a value at least 13, but was 11 |
must be at least 13, but was 11 |
value must be at least 13, but was 11 |
| opaque prose | prose unchanged | prose unchanged | noun composed once around the prose |
| group | legacy ; and , or join |
localized group composed once | noun composed once around the group |
Localized English uses bare predicates and composition, which reads better in a sentence. Exact wording
compatibility is promised for render only.