Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultParsing
Maturity: Incubating
Reified.Parse converts serialized text into primitive typed values. "42" becomes 42, "true" becomes true.
Unlike Int32.TryParse, it returns a value identifying which of three failures occurred.
dotnet add package Reified.Parse
open Reified
Parse.int "12" // Ok 12
Parse.int "" // Error (MissingValue "int")
Parse.int "twelve" // Error (InvalidFormat ("int", "twelve"))
Parse.int "99999999999" // Error (OutOfRange ("int", "99999999999"))
ReifiedReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
ParseError is an independent leaf type. It carries no prose and no culture, so it stays comparable data you can map
into your own error case, assert on in a test, or pass across a boundary.
The three cases
type ParseError =
| MissingValue of target: string
| InvalidFormat of target: string * input: string
| OutOfRange of target: string * input: string
FsLiveDocsGeneratedPage41_6BB268B4E740.ParseErrorMissingValuetarget: stringstringAn abbreviation for the CLI type . Basic Types
InvalidFormatinput: stringOutOfRange| Case | What it means | Example |
|---|---|---|
MissingValue target |
The input was absent, empty, or only whitespace. There was nothing to convert. | Parse.int " " → MissingValue "int" |
InvalidFormat (target, input) |
Text was supplied but does not spell a value of that type. | Parse.bool "yes" → InvalidFormat ("bool", "yes") |
OutOfRange (target, input) |
Text spells a well-formed number the destination type cannot hold. | Parse.int "99999999999" → OutOfRange ("int", "99999999999") |
target names the destination type, "int", "bool", "decimal", "Guid", so a caller can tell which conversion
failed without tracking it separately. input is the offending text, retained for redisplay.
An empty field and a misspelled one can need different messages. OutOfRange identifies a well-formed number outside the destination type's range, either too large or too small: Parse.int "-2147483649" reports OutOfRange ("int", "-2147483649").
The parsers
The common parsers:
| Parser | Returns | Accepts |
|---|---|---|
Parse.int |
int |
Optionally signed digits, invariant culture. |
Parse.decimal |
decimal |
Digits with an optional sign, decimal point, and thousands separators, invariant culture. |
Parse.bool |
bool |
"true" or "false", any casing. Not "1", "yes", or "on". |
Parse.guid |
System.Guid |
Any format Guid.TryParse accepts, including braced and hyphenless. |
Parse.enum<'enum> |
'enum |
A case name, case-insensitive, or its numeric text. |
The complete table — long, float, dateTime, dateTimeOffset, dateOnly, timeOnly, and the exact accepted
form of every parser — is in the Parse API reference.
Every parser takes string and returns Result<'value, ParseError>. Numeric parsers use invariant culture: the same text parses the same way on every machine, and the decimal point is ., not ,. They also ignore leading and trailing whitespace; whitespace-only input is still MissingValue.
Only Parse.int, Parse.long, and Parse.decimal can produce OutOfRange; every other parser reports MissingValue or InvalidFormat. Parse.float never reports OutOfRange: input beyond float's range parses as infinity or -infinity, and input too close to zero parses as 0.0.
The three failures in practice
// Missing: nothing was supplied.
Parse.decimal ""
// Error (MissingValue "decimal")
// Malformed: something was supplied, but it is not a decimal.
Parse.decimal "19,95 AUD"
// Error (InvalidFormat ("decimal", "19,95 AUD"))
// Out of range: well-formed, but outside the type's range.
Parse.int "2147483648"
// Error (OutOfRange ("int", "2147483648"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
decimal: string -> Result<decimal,ParseError>Parses a decimal number.
int: string -> Result<int,ParseError>Parses a 32-bit integer.
Match on the case when the three need different treatment:
let describe error =
match error with
| MissingValue target -> $"Please supply a {target}."
| InvalidFormat (_, input) -> $"'{input}' is not in the expected format."
| OutOfRange (_, input) -> $"'{input}' is out of range."
describe: ParseError -> stringerror: ParseErrorMissingValuetarget: stringInvalidFormatinput: stringOutOfRangeHand off to your own error type
Map ParseError into an application error at the point of use when callers do not need the parse details:
type PortError = PortNotANumber of string
let port raw : Result<int, PortError> =
Parse.int raw |> Result.mapError (fun _ -> PortNotANumber raw)
FsLiveDocsGeneratedPage41_6BB268B4E740.PortErrorPortNotANumberstringAn abbreviation for the CLI type . Basic Types
port: string -> Result<int,PortError>raw: stringMicrosoft.FSharp.Core.FSharpResult`2Helper type for error handling without exceptions. Choices and Results
intAn abbreviation for the CLI type . Basic Types
Reified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses 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
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
mapError: ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>Maps the error value of a result.
Result.orError from Reified.Result is shorter when the error case carries nothing:
type MissingPort = PortMissing
Parse.int "abc" |> Result.orError PortMissing
FsLiveDocsGeneratedPage41_6BB268B4E740.MissingPortPortMissingReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses 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
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
orError: 'error -> Result<'value,'discardedError> -> Result<'value,'error>Replaces whatever error a result carries with the supplied typed error. Ok passes through unchanged. The natural follow-up to okIf/failIf, which fail with unit precisely so the reason is chosen here: value |> Result.okIf isValid |> Result.orError MyError. Use Result.mapError instead when the existing error carries something worth keeping, as a Violation does.
When callers need to distinguish the three failures or redisplay the text, use Result.mapError InvalidInteger to keep ParseError in an application error case.
For structured input, Schema parses each field, accumulates every failure, and reports the field path alongside it.
Optional input
Parse.optional distinguishes an absent value from a bad one. Absence succeeds as None; malformed present text still
fails.
Parse.optional Parse.int None
// Ok None
Parse.optional Parse.int (Some "42")
// Ok (Some 42)
Parse.optional Parse.int (Some "bad")
// Error (InvalidFormat ("int", "bad"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
optional: ('raw -> Result<'value,'error>) -> 'raw option -> Result<'value option,'error>Parses an optional input, preserving a present input's parsing failure. Parse.optional Parse.int None = Ok None Parse.optional Parse.int (Some "42") = Ok (Some 42) Parse.optional Parse.int (Some "bad") = Error (ParseError.InvalidFormat ("int", "bad"))
int: string -> Result<int,ParseError>Parses a 32-bit integer.
NoneThe representation of "No value"
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
Parse.optionalOr supplies a fallback only when the input is absent:
Parse.optionalOr 80 Parse.int None
// Ok 80
Parse.optionalOr 80 Parse.int (Some "443")
// Ok 443
Parse.optionalOr 80 Parse.int (Some "bad")
// Error (InvalidFormat ("int", "bad"))
Reified.ParseModulePrimitive parsers for untrusted serialized input.
optionalOr: 'value -> ('raw -> Result<'value,'error>) -> 'raw option -> Result<'value,'error>Parses an optional input, using the supplied fallback only when the input is absent. Parse.optionalOr 80 Parse.int None = Ok 80 Parse.optionalOr 80 Parse.int (Some "443") = Ok 443 Parse.optionalOr 80 Parse.int (Some "bad") = Error (ParseError.InvalidFormat ("int", "bad"))
int: string -> Result<int,ParseError>Parses a 32-bit integer.
NoneThe representation of "No value"
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
The fallback applies to omitted input, not malformed input.
A present-but-blank string is a present input, so it parses (and fails) rather than taking the fallback. The primitive parsers treat empty or whitespace-only text as a missing value, not a format error:
Parse.optionalOr 80 Parse.int (Some "")
// Error (MissingValue "int")
Parse.optionalOr 80 Parse.int (Some " ")
// Error (MissingValue "int")
Reified.ParseModulePrimitive parsers for untrusted serialized input.
optionalOr: 'value -> ('raw -> Result<'value,'error>) -> 'raw option -> Result<'value,'error>Parses an optional input, using the supplied fallback only when the input is absent. Parse.optionalOr 80 Parse.int None = Ok 80 Parse.optionalOr 80 Parse.int (Some "443") = Ok 443 Parse.optionalOr 80 Parse.int (Some "bad") = Error (ParseError.InvalidFormat ("int", "bad"))
int: string -> Result<int,ParseError>Parses a 32-bit integer.
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
Some "" and Some " " produce MissingValue, never Ok 80. Only None receives the fallback.
Combined optional helpers
Named helpers pair the common primitive parsers with optional and optionalOr; each preserves the error from
malformed present text:
| Family | Absence | Default |
|---|---|---|
Parse.int |
intOption → Ok None |
intOrDefault value → Ok value |
Parse.bool |
boolOption → Ok None |
boolOrDefault value → Ok value |
Parse.decimal |
decimalOption → Ok None |
decimalOrDefault value → Ok value |
Parse.guid |
guidOption → Ok None |
guidOrDefault value → Ok value |
Use *Option when absence should stay None, and *OrDefault when absence should become a concrete value.
Parse, then refine
Parsing changes representation; a refinement then admits the typed value into a domain type by checking a constraint. Here Parse.int converts text to int, and Refinement.create positiveInt admits it as a PositiveInt. Each step contributes its own error:
open Reified
open Reified.Refinements
open Reified.Result
open Reified.ResultDSL
type PositiveInt = PositiveInt of int
let positiveInt =
Refinement.define (Constraint.greaterThan 0) PositiveInt (fun (PositiveInt value) -> value)
type QuantityError =
| InvalidInteger of ParseError
| InvalidQuantity of Violation
let quantity raw =
result {
let! parsed = Parse.int raw |> Result.mapError InvalidInteger
let! quantity = parsed |> Refinement.create positiveInt |> Result.mapError InvalidQuantity
return quantity
}
ReifiedRefinementsResultReified.ResultDSLThe concise result vocabulary: the result { } computation expression, its accumulating result.list { } / result.array { } variants, and the lightweight admission functions (okIf, failIf, require, orError, mapError). Optional and opt-in, in the same shape as Reified.DataDSL, Reified.ConstraintDSL, and Reified.SchemaDSL: open Reified.Result for Result, then open Reified.ResultDSL for this vocabulary. Deliberately small: generic combinators such as map, bind, orElse, tap, and the traversal helpers stay qualified as Result.map, Result.bind, and so on.
FsLiveDocsGeneratedPage41_6BB268B4E740.PositiveIntPositiveIntintAn abbreviation for the CLI type . Basic Types
positiveInt: Refinement<int,PositiveInt>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: PositiveIntvalue: intFsLiveDocsGeneratedPage41_6BB268B4E740.QuantityErrorInvalidIntegerReified.ParseErrorPrimitive parse failures returned by Parse helpers.
InvalidQuantityReified.ViolationWhy a value failed its constraint. A diagnostic contract, not an application error union. Domain code maps a whole violation once with Result.mapError; Schema adds the path at which it occurred. Violations are plain comparable data. No closure and no constraint description is reachable from one, so structural equality holds and a violation can be retained and compared long after the constraint that produced it went out of scope. There is no promised wire format. Reified-produced groups are never empty and never unary: a single failing child is returned directly rather than wrapped. The first * rest shape encodes non-emptiness only; non-unarity is a normalization invariant.
quantity: string -> Result<PositiveInt,QuantityError>raw: stringresult: ResultBuilderThe fail-fast result { } computation expression.
parsed: intReified.ParseModulePrimitive parsers for untrusted serialized input.
int: string -> Result<int,ParseError>Parses 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
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
mapError: ('a -> 'b) -> Result<'c,'a> -> Result<'c,'b>Maps the error value of a result.
quantity: PositiveIntcreate: 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
See Refined values for the refinement model, and the Parse API reference for every parser.