Repository F# setup
open Reified
open Reified.Refinements
open Reified.ResultOrder Totals Tutorial
This tutorial builds an order from untrusted input and then calculates over it. Each invariant admitted at the boundary removes a branch, an option, or a guard from downstream code.
open System
open Reified
open Reified.Refinements
open Reified.Result
open Reified.ResultDSL
SystemReifiedRefinementsResultReified.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.
Requires & types
Requires: (packages: Reified.Result)Model the domain
type OrderLine =
{ Sku: NonBlankString
Quantity: int
UnitPrice: decimal }
type Order =
{ Reference: NonBlankString
Lines: NonEmptyList<OrderLine>
Discount: UnitInterval
Delivery: Interval<DateTimeOffset> }
FsLiveDocsGeneratedPage35_163BCE492C58.OrderLineSku: NonBlankStringReified.Refinements.NonBlankStringA string that is not null, empty, or whitespace.
Quantity: intintAn abbreviation for the CLI type . Basic Types
UnitPrice: decimaldecimalAn abbreviation for the CLI type . Basic Types
FsLiveDocsGeneratedPage35_163BCE492C58.OrderReference: NonBlankStringLines: NonEmptyList<OrderLine>Reified.Refinements.NonEmptyList`1A list that contains at least one item. The case is public: non-emptiness is carried by the representation rather than by a checked constructor, so head, last, reduce, min, and max are total and pattern matching is available to callers.
Discount: UnitIntervalReified.Refinements.UnitIntervalA finite double between zero and one inclusive. The only type in this package closed under multiplication: a product of two values in [0, 1] is always in [0, 1], with no overflow to guard against. It is not closed under addition — 0.7 + 0.7 leaves the range — so add is deliberately absent in favour of saturatingAdd and complement.
Delivery: Interval<DateTimeOffset>Reified.Refinements.Interval`1An inclusive range of ordered values where Lower <= Upper. An interval is always inhabited. Emptiness is represented by Interval option, which is what intersect returns, rather than by a second type — carrying a possibly-empty interval would double every operation without making any of them total. The two ends are named for their roles as bounds, not for a traversal: an interval has no direction, and between 5 1 equals between 1 5. Wire formats that read better as start/end choose those field names at the schema, which is independent of these members — see RefinedSchemas.dateRange. The invariant assumes the value type is totally ordered. float and float32 are not: NaN compares false against everything, so between nan x cannot order its arguments and yields an interval whose bounds are inverted. Use Interval<FiniteFloat>, which excludes NaN by construction, or create, which rejects the pair. This is the same defect exists to remove.
System.DateTimeOffsetRepresents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).
Requires & types
Requires: open Reified.Refinements; open System (packages: Reified.Refinements)The refined field types specify the order's invariants. An order has at least one line. The discount is a
proportion, so it cannot be 140% or NaN. The delivery window's start is not after its
end. None of those facts needs restating later, because none of them can be false.
Quantity and price are ordinary numbers, so their "greater than zero" rule lives in the
orderLine constructor below rather than in the type: a caller who builds an OrderLine
record directly can still put a negative quantity or price in it. They are not refined
types because F# cannot carry "greater than zero" through arithmetic, see
why there are no refined numbers.
Admit the input
let orderLine rawSku rawQuantity rawPrice =
result {
let! sku = Refine.nonBlankString rawSku
let! _ = Constraint.check (Constraint.greaterThan 0) rawQuantity
let! _ = Constraint.check (Constraint.greaterThan 0m) rawPrice
return { Sku = sku; Quantity = rawQuantity; UnitPrice = rawPrice }
}
orderLine: string -> int -> decimal -> Result<OrderLine,Violation>rawSku: stringrawQuantity: intrawPrice: decimalresult: ResultBuilderThe fail-fast result { } computation expression.
sku: NonBlankStringReified.Refinements.RefineModuleSmart constructors for built-in refined values, and the refinements behind them. Text, Character, Collection, and Choice are nested here rather than declared beside Refine. Each names a concept general enough to shadow something a caller already has in scope, and none of them is a companion to a type this package exports, so there is nothing to gain from the type-and-module pairing that keeps NonBlankString and DistinctList at the top level.
nonBlankString: string -> Result<NonBlankString,Violation>Reified.ConstraintModuleCreates, executes, composes, and inspects constraints.
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
greaterThan: 'value -> Constraint<'value>Requires a value strictly greater than the supplied bound. let quantity : Constraint<int> = Constraint.greaterThan 0
Sku: NonBlankStringQuantity: intUnitPrice: decimalRequires & types
Requires: open Reified; open Reified.Refinements; open Reified.ResultDSL (packages: Reified.Constraint, Reified.Refinements, Reified.Result)val orderLine: string -> int -> decimal -> Result<OrderLine,Violation>The orderLine constructor is where quantity and price are admitted, and each refined constructor rejects its own invalid input:
orderLine "SKU-1" 0 9.99m // Error, quantity must be positive
orderLine " " 1 9.99m // Error, sku is blank
Refine.nonEmptyList ([]: OrderLine list)
// Error, an order needs at least one line
UnitInterval.create 1.4 // Error, outside [0, 1]
UnitInterval.create Double.NaN // Error, NaN is outside every interval
orderLine: string -> int -> decimal -> Result<OrderLine,Violation>Reified.Refinements.RefineModuleSmart constructors for built-in refined values, and the refinements behind them. Text, Character, Collection, and Choice are nested here rather than declared beside Refine. Each names a concept general enough to shadow something a caller already has in scope, and none of them is a companion to a type this package exports, so there is nothing to gain from the type-and-module pairing that keeps NonBlankString and DistinctList at the top level.
nonEmptyList: 'a seq -> Result<NonEmptyList<'a>,Violation>FsLiveDocsGeneratedPage35_163BCE492C58.OrderLinelistThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
Reified.Refinements.UnitIntervalModuleOperations over proportions between zero and one.
create: float -> Result<UnitInterval,Violation>Admits a proportion, rejecting anything outside [0, 1] including NaN.
System.DoubleRepresents a double-precision floating-point number.
NaN: floatRepresents a value that is not a number (). This field is constant.
Requires & types
Requires: open Reified.Refinements; open System (packages: Reified.Refinements)All four field constructors fail with the same Violation type, so one result { } block admits the whole order
without an error-union mapping:
let order
rawReference
(rawLines: (string * int * decimal) list)
rawDiscount
rawFrom
rawTo
=
result {
let! reference = Refine.nonBlankString rawReference
let! lines =
rawLines
|> Result.traverse (fun (sku, quantity, price) -> orderLine sku quantity price)
|> Result.bind Refine.nonEmptyList
let! discount = UnitInterval.create rawDiscount
let! delivery = Interval.create rawFrom rawTo
return
{ Reference = reference
Lines = lines
Discount = discount
Delivery = delivery }
}
order: string -> (string * int * decimal) list -> float -> DateTimeOffset -> DateTimeOffset -> Result<Order,Violation>rawReference: stringrawLines: (string * int * decimal) liststringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
decimalAn abbreviation for the CLI type . Basic Types
listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
rawDiscount: floatrawFrom: DateTimeOffsetrawTo: DateTimeOffsetresult: ResultBuilderThe fail-fast result { } computation expression.
reference: NonBlankStringReified.Refinements.RefineModuleSmart constructors for built-in refined values, and the refinements behind them. Text, Character, Collection, and Choice are nested here rather than declared beside Refine. Each names a concept general enough to shadow something a caller already has in scope, and none of them is a companion to a type this package exports, so there is nothing to gain from the type-and-module pairing that keeps NonBlankString and DistinctList at the top level.
nonBlankString: string -> Result<NonBlankString,Violation>lines: NonEmptyList<OrderLine>(|>): '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.
traverse: ('input -> Result<'output,'error>) -> 'input seq -> Result<'output list,'error>Maps each value with a result-returning function, stopping at the first error. Takes any sequence and always produces a list. Traversal stops at the first error, so later mappings do not run. Use one of the accumulating builders when every error should be reported. [ "1"; "2" ] |> Result.traverse parseInt // Ok [ 1; 2 ]
sku: stringquantity: intprice: decimalorderLine: string -> int -> decimal -> Result<OrderLine,Violation>bind: ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>Binds a result to the next fail-fast operation.
nonEmptyList: 'a seq -> Result<NonEmptyList<'a>,Violation>discount: UnitIntervalReified.Refinements.UnitIntervalModuleOperations over proportions between zero and one.
create: float -> Result<UnitInterval,Violation>Admits a proportion, rejecting anything outside [0, 1] including NaN.
delivery: Interval<DateTimeOffset>Reified.Refinements.IntervalModuleOperations over inclusive ranges of ordered values.
create: 'value -> 'value -> Result<Interval<'value>,Violation>Builds an interval from a pair the caller asserts is already ordered, failing when it is not. Use this at a boundary, where an inverted pair is a caller error worth reporting rather than silently repairing; use between when either order is acceptable input.
Reference: NonBlankStringLines: NonEmptyList<OrderLine>Discount: UnitIntervalDelivery: Interval<DateTimeOffset>Requires & types
Requires: open Reified.Refinements; open Reified.Result; open Reified.ResultDSL (packages: Reified.Refinements, Reified.Result)val order: string -> (string * int * decimal) list -> float -> DateTimeOffset -> DateTimeOffset -> Result<Order,Violation>Result.traverse runs orderLine over every raw line and stops at the first failure; Refine.nonEmptyList then
checks that the collected lines form a non-empty list, so an empty order is rejected before the Order exists.
let orderFrom = DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero)
let orderTo = DateTimeOffset(2026, 6, 5, 0, 0, 0, TimeSpan.Zero)
order "ORD-1" [ "SKU-1", 2, 9.99m ] 0.15 orderFrom orderTo
// Ok { Reference = …; Lines = …; Discount = …; Delivery = … }
order "ORD-1" [] 0.15 orderFrom orderTo
// Error, an order needs at least one line
orderFrom: DateTimeOffset``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than DateTimeOffset.MinValue or later than DateTimeOffset.MaxValue.
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
orderTo: DateTimeOffsetorder: string -> (string * int * decimal) list -> float -> DateTimeOffset -> DateTimeOffset -> Result<Order,Violation>Requires & types
Requires: open Systemval orderFrom: DateTimeOffsetval orderTo: DateTimeOffsetTwo of the four fields have a total constructor, which is the one to prefer when the input has an obvious correct reading:
let requestedFrom = DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero)
let requestedTo = DateTimeOffset(2026, 6, 5, 0, 0, 0, TimeSpan.Zero)
let rawDiscount = 0.15
let window = Interval.between requestedFrom requestedTo // cannot fail: orders the pair
let discount = UnitInterval.clamp rawDiscount // cannot fail: clamps into [0, 1]
requestedFrom: DateTimeOffset``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than DateTimeOffset.MinValue or later than DateTimeOffset.MaxValue.
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
requestedTo: DateTimeOffsetrawDiscount: floatwindow: Interval<DateTimeOffset>Reified.Refinements.IntervalModuleOperations over inclusive ranges of ordered values.
between: 'a -> 'a -> Interval<'a>Builds the smallest interval containing both values, ordering them as needed. Total — this is the constructor to reach for first. Requires a total order. With float or float32, a NaN argument cannot be ordered against anything and produces inverted bounds; prefer Interval<FiniteFloat> or create there.
discount: UnitIntervalReified.Refinements.UnitIntervalModuleOperations over proportions between zero and one.
clamp: float -> UnitIntervalRestricts any double into [0, 1]. Total, including for NaN, which clamps to zero because it has no meaningful position in the range.
Requires & types
Requires: open Reified.Refinements; open System (packages: Reified.Refinements)val requestedFrom: DateTimeOffsetval requestedTo: DateTimeOffsetval rawDiscount: floatval window: Interval<DateTimeOffset>val discount: UnitIntervalInterval.between accepts the two instants in either order. Use Interval.create instead
when an inverted pair means the caller made a mistake you would rather report than repair.
Calculate, without re-checking anything
Line and order totals
The numbers are plain, so the arithmetic is plain:
let lineTotal (line: OrderLine) = decimal line.Quantity * line.UnitPrice
let subtotal (order: Order) =
order.Lines |> NonEmptyList.map lineTotal |> NonEmptyList.reduce (+)
lineTotal: OrderLine -> decimalline: OrderLineFsLiveDocsGeneratedPage35_163BCE492C58.OrderLinedecimal: ^T -> decimalConverts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using UInt64.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted decimal. decimal "42.23" // evaluates to 42.23M decimal 0xff // evaluates to 255M decimal -10 // evaluates to -10M
Quantity: int(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
UnitPrice: decimalsubtotal: Order -> decimalorder: OrderFsLiveDocsGeneratedPage35_163BCE492C58.OrderLines: NonEmptyList<OrderLine>(|>): '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.Refinements.NonEmptyListModuleOperations over lists that carry their non-emptiness in the type.
map: ('value -> 'a) -> NonEmptyList<'value> -> NonEmptyList<'a>Applies a mapping to every item. Non-emptiness is preserved.
reduce: ('value -> 'value -> 'value) -> NonEmptyList<'value> -> 'valueCombines every item with an associative operation. Total — no seed required.
(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "Word" // Evaluates to "Hello World"
Requires & types
Requires: open Reified.Refinements (packages: Reified.Refinements)val lineTotal: OrderLine -> decimalval subtotal: Order -> decimalreduce needs no seed or empty case, and adds no Result between operations.
Discount
let payable (order: Order) =
let multiplier = UnitInterval.complement order.Discount
subtotal order * decimal (UnitInterval.value multiplier)
payable: Order -> decimalorder: OrderFsLiveDocsGeneratedPage35_163BCE492C58.Ordermultiplier: UnitIntervalReified.Refinements.UnitIntervalModuleOperations over proportions between zero and one.
complement: UnitInterval -> UnitIntervalReturns the distance to one. Total and closed. An involution only up to floating-point rounding: the round trip is exact for dyadic values such as 0.25, but 1 - (1 - 0.3) is not 0.3 in IEEE 754. Compare with a tolerance rather than for equality.
Discount: UnitIntervalsubtotal: Order -> decimal(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
decimal: ^T -> decimalConverts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using UInt64.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted decimal. decimal "42.23" // evaluates to 42.23M decimal 0xff // evaluates to 255M decimal -10 // evaluates to -10M
value: UnitInterval -> floatReturns the underlying value.
Requires & types
Requires: open Reified.Refinements (packages: Reified.Refinements)val payable: Order -> decimalcomplement is total and closed, so multiplier is guaranteed to be in [0, 1]. For an
order admitted through orderLine, quantity and price are positive, so the subtotal is
nonnegative and the payable amount cannot exceed the subtotal or go negative — no discount
value allows either. That guarantee comes from admission, not from the record types: a
directly constructed OrderLine with a negative price breaks it, and decimal arithmetic
can still overflow on very large values.
The conversion to decimal is deliberate rather than hidden. UnitInterval is a double,
money is a decimal, and mixing the two is a rounding decision that belongs in your code.
Statistics
let largestLine (order: Order) =
order.Lines |> NonEmptyList.maxBy (fun line -> line.Quantity)
let lineCount (order: Order) =
NonEmptyList.length order.Lines
let averageUnitPrice (order: Order) =
order.Lines |> NonEmptyList.averageBy (fun line -> line.UnitPrice)
largestLine: Order -> OrderLineorder: OrderFsLiveDocsGeneratedPage35_163BCE492C58.OrderLines: NonEmptyList<OrderLine>(|>): '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.Refinements.NonEmptyListModuleOperations over lists that carry their non-emptiness in the type.
maxBy: ('value -> 'a) -> NonEmptyList<'value> -> 'valueReturns the item with the largest projected key. Total.
line: OrderLineQuantity: intlineCount: Order -> intlength: NonEmptyList<'value> -> intReturns the number of items as a plain int, matching List.length.
averageUnitPrice: Order -> decimalaverageBy: ('value -> ^a) -> NonEmptyList<'value> -> ^aAverages a projection of the items. Total, for the same reason as average.
UnitPrice: decimalRequires & types
Requires: open Reified.Refinements (packages: Reified.Refinements)val largestLine: Order -> OrderLineval lineCount: Order -> intval averageUnitPrice: Order -> decimalmaxBy returns an OrderLine, not an option. averageBy returns a decimal, not an
option, because the divisor is the length and the length is at least one. Each of those is
a branch the plain-list version would have had to write. For contrast only, not part of the
running example:
type OrderLine = { Quantity: int; UnitPrice: decimal }
// what the same three functions cost over an ordinary list
let largestLine lines = lines |> List.sortByDescending (fun l -> l.Quantity) |> List.tryHead
let averageUnitPrice lines =
if List.isEmpty lines then None
else Some (List.sumBy (fun (l: OrderLine) -> l.UnitPrice) lines / decimal (List.length lines))
FsLiveDocsGeneratedPage36_163BCE492C58.OrderLineQuantity: intintAn abbreviation for the CLI type . Basic Types
UnitPrice: decimaldecimalAn abbreviation for the CLI type . Basic Types
largestLine: OrderLine list -> OrderLine optionlines: OrderLine list(|>): '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.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
sortByDescending: ('T -> 'Key) -> 'T list -> 'T listSorts the given list in descending order using keys given by the given projection. Keys are compared using . This is a stable sort, i.e. the original order of equal elements is preserved. The function to transform the list elements into the type to be compared. The input list. The sorted list. let input = ["a"; "bbb"; "cccc"; "dd"] input |> List.sortByDescending (fun s -> s.Length) Evaluates to ["cccc"; "bbb"; "dd"; "a"].
l: OrderLinetryHead: 'T list -> 'T optionReturns the first element of the list, or None if the list is empty. The input list. The first element of the list or None. let inputs = [ "banana"; "pear" ] inputs |> List.tryHead Evaluates to Some "banana" let inputs : int list = [] inputs |> List.tryHead Evaluates to None
averageUnitPrice: OrderLine list -> decimal optionisEmpty: 'T list -> boolReturns true if the list contains no elements, false otherwise. The input list. True if the list is empty. [ ] |> List.isEmpty Evaluates to true [ "pear"; "banana" ] |> List.isEmpty Evaluates to false
NoneThe representation of "No value"
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
sumBy: ('T -> ^U) -> 'T list -> ^UReturns the sum of the results generated by applying the function to each element of the list. The function to transform the list elements into the type to be summed. The input list. The resulting sum. let input = [ "aa"; "bbb"; "cc" ] input |> List.sumBy (fun s -> s.Length) Evaluates to 7.
(/): ^T1 -> ^T2 -> ^T3Overloaded division operator The first parameter. The second parameter. The result of the operation. 16 / 2 // Evaluates to 8
decimal: ^T -> decimalConverts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using UInt64.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted decimal. decimal "42.23" // evaluates to 42.23M decimal 0xff // evaluates to 255M decimal -10 // evaluates to -10M
length: 'T list -> intReturns the length of the list. The input list. The length of the list. The notation array.Length is preferred. let inputs = [ "a"; "b"; "c" ] inputs |> List.length Evaluates to 3
The collection modules carry the everyday operations (sum, sumBy, average, choose, countBy) as well as
the total ones, so nothing here converts back to a list to do ordinary work.
Delivery window
let isDeliverable (order: Order) (candidate: DateTimeOffset) =
Interval.contains candidate order.Delivery
let overlapWith (order: Order) (other: Interval<DateTimeOffset>) =
Interval.intersect order.Delivery other // Interval option, None when disjoint
isDeliverable: Order -> DateTimeOffset -> boolorder: OrderFsLiveDocsGeneratedPage35_163BCE492C58.Ordercandidate: DateTimeOffsetSystem.DateTimeOffsetRepresents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).
Reified.Refinements.IntervalModuleOperations over inclusive ranges of ordered values.
contains: 'value -> Interval<'value> -> boolReturns whether the value lies within the inclusive bounds.
Delivery: Interval<DateTimeOffset>overlapWith: Order -> Interval<DateTimeOffset> -> Interval<DateTimeOffset> optionother: Interval<DateTimeOffset>Reified.Refinements.Interval`1An inclusive range of ordered values where Lower <= Upper. An interval is always inhabited. Emptiness is represented by Interval option, which is what intersect returns, rather than by a second type — carrying a possibly-empty interval would double every operation without making any of them total. The two ends are named for their roles as bounds, not for a traversal: an interval has no direction, and between 5 1 equals between 1 5. Wire formats that read better as start/end choose those field names at the schema, which is independent of these members — see RefinedSchemas.dateRange. The invariant assumes the value type is totally ordered. float and float32 are not: NaN compares false against everything, so between nan x cannot order its arguments and yields an interval whose bounds are inverted. Use Interval<FiniteFloat>, which excludes NaN by construction, or create, which rejects the pair. This is the same defect exists to remove.
intersect: Interval<'value> -> Interval<'value> -> Interval<'value> optionReturns the shared portion of two intervals, or None when they are disjoint. The option is the honest representation of an empty result.
Requires & types
Requires: open Reified.Refinements; open System (packages: Reified.Refinements)val isDeliverable: Order -> DateTimeOffset -> boolval overlapWith: Order -> Interval<DateTimeOffset> -> Interval<DateTimeOffset> optionintersect returns an option because two windows may not overlap. An empty interval is not representable, so the option reports emptiness without creating a value whose Lower is above its Upper.
Catch a duplicate the type system can see
Distinctness is a relationship between values, so it needs a checked constructor, but the resulting type then converts to a map without silently dropping entries:
let skus (order: Order) =
order.Lines
|> NonEmptyList.map (fun line -> NonBlankString.value line.Sku)
|> NonEmptyList.toList
|> DistinctList.create // Error, rejected: the same SKU appears twice
let lineBySku (order: Order) =
order.Lines
|> NonEmptyList.toList
|> List.map (fun line -> NonBlankString.value line.Sku, line)
|> DistinctList.create
|> Result.bind DistinctList.toMap
skus: Order -> Result<DistinctList<string>,Violation>order: OrderFsLiveDocsGeneratedPage35_163BCE492C58.OrderLines: NonEmptyList<OrderLine>(|>): '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.Refinements.NonEmptyListModuleOperations over lists that carry their non-emptiness in the type.
map: ('value -> 'a) -> NonEmptyList<'value> -> NonEmptyList<'a>Applies a mapping to every item. Non-emptiness is preserved.
line: OrderLineReified.Refinements.NonBlankStringModuleOperations over text known to carry non-whitespace content.
value: NonBlankString -> stringReturns the underlying string value.
Sku: NonBlankStringtoList: NonEmptyList<'value> -> 'value listReturns the refined value as a standard list.
Reified.Refinements.DistinctListModuleOperations over lists known to hold no duplicates.
create: 'a seq -> Result<DistinctList<'a>,Violation>Admits a list, failing when it holds duplicates.
lineBySku: Order -> Result<Map<string,OrderLine>,Violation>Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ]
Reified.Result.ResultModuleFail-fast helpers over the standard F# Result type.
bind: ('a -> Result<'b,'c>) -> Result<'a,'c> -> Result<'b,'c>Binds a result to the next fail-fast operation.
toMap: DistinctList<'key * 'value> -> Result<Map<'key,'value>,Violation>Builds a map from a distinct list of pairs, failing when two pairs share a key. Distinctness holds over whole pairs, not over keys: [ 1, "a"; 1, "b" ] is a legitimate DistinctList whose entries would collide in a map. The check is what makes the conversion lossless — Map.ofList would silently keep one. For the unconditional guarantee use toSet, where distinct elements always produce a set of the same size.
Requires & types
Requires: open Reified.Refinements; open Reified.Result (packages: Reified.Refinements, Reified.Result)val skus: Order -> Result<DistinctList<string>,Violation>val lineBySku: Order -> Result<Map<string,OrderLine>,Violation>Map.ofList on an ordinary list keeps only the last of each duplicate key and reports
nothing. DistinctList.toMap returns a Result instead: distinctness holds over whole
pairs rather than over keys, so it checks the keys and tells you about a collision rather
than losing an entry.
What the invariants removed
| Fact carried by a type | Branch it removed |
|---|---|
NonEmptyList has a first item |
no tryHead, no option from max/reduce |
NonEmptyList has a positive length |
no divide-by-zero on an average |
UnitInterval is in [0, 1] |
no clamping the multiplier before applying it |
Interval has Lower <= Upper |
no "did they send these backwards" check |
DistinctList has no duplicates |
no silent collapse building a set; a reported failure building a map |
NonBlankString is never empty or whitespace |
no blank-string guard at every read site |
Next
- Built-in Refined Values, what each type is closed under.
- Customer Id, define a refined type of your own, and give it a schema.
- Compose Parse and Refinement, mapping failures to application errors.