Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result

Working with violations

Constraint.check returns Error violation when a value fails. A Violation is structured diagnostic data; render it for a person, or keep it structured when application code needs to classify or translate the failure.

Render an English message

Use Violation.render at the edge where the failure becomes text:

Verified
open Reified

let retryCount : Constraint<int> =
    Constraint.between 0 10

33
|> Constraint.check retryCount
|> Result.mapError Violation.render
// Error "expected a value between 0 and 10, but was 33"
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)
val retryCount: Constraint<int>

When handling the result directly:

Verified
match 33 |> Constraint.check retryCount with
| Ok () -> printfn "valid"
| Error violation -> printfn "%s" (Violation.render violation)
Requires & types
Requires: open Reified (packages: Reified.Constraint)

render returns an English sentence fragment with no trailing punctuation. Add punctuation or a field label in the presentation layer that owns the complete message.

Keep the violation in application errors

Do not turn every constraint failure into a string immediately. An application error can retain the Violation and choose how to present it later:

Verified
type SignupError =
    | InvalidName of Violation

let name : Constraint<string> =
    Constraint.all [ Constraint.present; Constraint.lengthBetween 2 40 ]

let validateName value =
    value
    |> Constraint.guard name
    |> Result.mapError InvalidName
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)
val name: Constraint<string>
val validateName: string -> Result<string,SignupError>

Render at the UI, log, or HTTP boundary:

Verified
let describe error =
    match error with
    | InvalidName violation -> $"Invalid name: {Violation.render violation}."
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val describe: SignupError -> string

This preserves the diagnostic for comparison, testing, localization, and other projections. Violation is not an application error union; wrap it in the case that identifies the failed application operation or field.

Multiple failures preserve their meaning

Constraint.all reports every failed child. Violation.render separates those failures with ; :

Verified
""
|> Constraint.check name
|> Result.mapError Violation.render
// Error "value must be present; expected a size between 2 and 40, but was 0"
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)

Constraint.any reports the rejected alternatives only when every alternative fails. Rendering separates alternatives with , or :

Verified
let ttl : Constraint<int> =
    Constraint.any (Constraint.equalTo -1) [ Constraint.positive ]

0
|> Constraint.check ttl
|> Result.mapError Violation.render
// Error "expected -1, but was 0, or expected a value greater than 0, but was 0"
Requires & types
Requires: open Reified; open Reified.Result (packages: Reified.Constraint, Reified.Result)
val ttl: Constraint<int>

The separators reflect the constraint tree: all means every reported condition was required, while any means the value failed every permitted alternative.

Inspect a violation without parsing its message

Built-in failures carry the failing constraint atom and, when Reified can represent it, the actual value. Use the projections when code needs those facts:

Verified
match "ab" |> Constraint.check (Constraint.minLength 3: Constraint<string>) with
| Ok () -> ()
| Error violation ->
    let expectation = Violation.tryExpectation violation
    // Some (CardinalityAtom (Cardinality.Minimum 3))

    let actual = Violation.tryActual violation
    // Some (ConstraintValue.Integer 2L)

    printfn "%A, %A" expectation actual
Requires & types
Requires: open Reified (packages: Reified.Constraint)

The projections return None for a grouped violation because a group has more than one answer. Use Violation.children for its immediate children, or Violation.flatten for every atomic failure in report order. Opaque rules created with Constraint.custom carry author-supplied prose instead; read a single opaque leaf with Violation.tryDescription.

Reified-produced groups are never empty or wrapped around one child. If only one child fails, that violation is returned directly.

Translate messages

Violation.render is the zero-dependency English default, not the only option. A Renderer carries the language and the document context; the violation carries the facts:

Verified
open Reified

let renderer = Renderer.english

let violation =
    match "" |> Constraint.check Constraint.present with
    | Error violation -> violation
    | Ok () -> failwith "unreachable"

let signup = renderer |> Renderer.context "signup"

violation |> Violation.message (signup |> Renderer.attribute "name")
// "must be present"

violation |> Violation.fullMessage (signup |> Renderer.attribute "name")
// "Name must be present"
Requires & types
Requires: open Reified (packages: Reified.Constraint)
val renderer: Renderer
val violation: Violation
val signup: Renderer

message renders a bare predicate, for a form row whose label already names the field. fullMessage composes the attribute noun once around the whole message, for payloads and logs. Renderer.english needs no resources at all.

Violation.toMessageTree remains available for a localization system that must control word order across a whole group. See Localization for rendering, with context and fallback, advanced rendering for groups and plurals, and the key catalogue. Adding a language covers generating a new translation.