FSharpPlus


Validation<'Error, 'T>

This is similar to Result<'T, 'Error> but with accumulative errors semantics, instead of short-circuit.

Examples

#r @"nuget: FSharpPlus"
open System
open FSharpPlus
open FSharpPlus.Data

module MovieValidations =
    type VError =
        | MustNotBeEmpty
        | MustBeAtLessThanChars of int
        | MustBeADate
        | MustBeOlderThan of int
        | MustBeWithingRange of decimal * decimal

    module String =
        let nonEmpty (x:string) : Validation<VError list, string> =
            if String.IsNullOrEmpty x
            then Failure [MustNotBeEmpty]
            else Success x
        let mustBeLessThan (i: int) (x: string) : Validation<VError list, string> =
            if isNull x || x.Length > i
            then Failure [MustBeAtLessThanChars i]
            else Success x

    module Number =
        let mustBeWithin (from, to') x =
            if from<= x && x <= to'
            then Success x
            else Failure [MustBeWithingRange (from, to')]
    
    module DateTime =
        let classicMovie year (d: DateTime) =
            if d.Year < year
            then Success d
            else Failure [MustBeOlderThan year]
        let date (d: DateTime) =
            if d.Date = d
            then Success d
            else Failure [MustBeADate]
    
    type Genre =
        | Classic
        | PostClassic
        | Modern
        | PostModern
        | Contemporary
    
    type Movie = {
        Id: int
        Title: String
        ReleaseDate: DateTime
        Description: String
        Price: decimal
        Genre: Genre
    } with
        static member Create (id, title, releaseDate, description, price, genre) : Validation<VError list, Movie> =
            fun title releaseDate description price -> { Id = id; Title = title; ReleaseDate = releaseDate; Description = description; Price = price; Genre = genre }
            <!> String.nonEmpty title <* String.mustBeLessThan 100 title
            <*> DateTime.classicMovie 1960 releaseDate <* DateTime.date releaseDate
            <*> String.nonEmpty description <* String.mustBeLessThan 1000 description
            <*> Number.mustBeWithin (0.0m, 999.99m) price

    let newRelease = Movie.Create (1, "Midsommar", DateTime (2019, 6, 24), "Midsommar is a 2019 folk horror film written...", 1m, Classic) //Failure [MustBeOlderThan 1960]
    let oldie = Movie.Create (2, "Modern Times", DateTime (1936, 2, 5), "Modern Times is a 1936 American comedy film...", 1m, Classic) // Success..
    let titleToLong = Movie.Create (3, String.Concat (seq { 1..110 }), DateTime (1950, 1, 1), "11", 1m, Classic) //Failure [MustBeAtLessThanChars 100]


module Person =

    type Name = { unName: String }
    with static member create s = {unName = s }

    type Email = { unEmail: String } 
    with static member create s = { unEmail = s }

    type Age = { unAge : int }
    with static member create i = { unAge = i }

    type Person = {
        name: Name
        email: Email
        age: Age }
    with static member create name email age = { name = name; email = email; age = age }


    type Error = 
        | NameBetween1And50
        | EmailMustContainAtChar
        | AgeBetween0and120

    // Smart constructors
    let mkName s =
        let l = length s
        if (l >= 1 && l <= 50)
        then Success <| Name.create s
        else Failure  [NameBetween1And50]

    let mkEmail s =
        if String.contains '@' s
        then Success <| Email.create s
        else Failure [EmailMustContainAtChar]

    let mkAge a =
        if (a >= 0 && a <= 120)
        then Success <| Age.create a
        else Failure [AgeBetween0and120]

    let mkPerson pName pEmail pAge =
        Person.create
        <!> mkName pName
        <*> mkEmail pEmail
        <*> mkAge pAge

    // Examples

    let validPerson = mkPerson "Bob" "bob@gmail.com" 25
    // Success ({name = {unName = "Bob"}; email = {unEmail = "bob@gmail.com"}; age = {unAge = 25}})

    let badName = mkPerson "" "bob@gmail.com" 25
    // Failure [NameBetween1And50]

    let badEmail = mkPerson "Bob" "bademail" 25
    // Failure [EmailMustContainAtChar]

    let badAge = mkPerson "Bob" "bob@gmail.com" 150
    // Failure [AgeBetween0and120]

    let badEverything = mkPerson "" "bademail" 150
    // Failure [NameBetween1And50;EmailMustContainAtChar;AgeBetween0and120]

    open FSharpPlus.Lens
    let asMaybeGood = validPerson ^? Validation._Success
    // Some ({name = {unName = "Bob"}; email = {unEmail = "bob@gmail.com"}; age = {unAge = 25}})
    let asMaybeBad = badEverything ^? Validation._Success
    // None

    let asResultGood = validPerson ^. Validation.isoValidationResult
    // Ok ({name = {unName = "Bob"}; email = {unEmail = "bob@gmail.com"}; age = {unAge = 25}})

    let asResultBad = badEverything ^. Validation.isoValidationResult
    // Error [NameBetween1And50;EmailMustContainAtChar;AgeBetween0and120]


module Email =

    // ***** Types *****
    type AtString = AtString of string
    type PeriodString = PeriodString of string
    type NonEmptyString = NonEmptyString of string

    type Email = Email of string

    type VError =
        | MustNotBeEmpty
        | MustContainAt
        | MustContainPeriod

    // ***** Base smart constructors *****
    // String must contain an '@' character
    let atString (x: string) : Validation<VError list, AtString> =
        if String.contains '@' x then Success <| AtString x
        else Failure [MustContainAt]

    // String must contain an '.' character
    let periodString (x: string) : Validation<VError list, PeriodString> =
        if String.contains '.' x
        then Success <| PeriodString x
        else Failure [MustContainPeriod]

    // String must not be empty
    let nonEmptyString (x: string) : Validation<VError list, NonEmptyString> =
        if not <| String.IsNullOrEmpty x
        then Success <| NonEmptyString x
        else Failure [MustNotBeEmpty]

    // ***** Combining smart constructors *****
    let email (x: string) : Validation<VError list, Email> =
        result (Email x) <*
        nonEmptyString x <*
        atString       x <*
        periodString   x

    // ***** Example usage *****
    let success = email "bob@gmail.com"

    // Success (Email "bob@gmail.com")

    let failureAt = email "bobgmail.com"
    // Failure [MustContainAt]

    let failurePeriod = email "bob@gmailcom"
    // Failure [MustContainPeriod]


    let failureAll = email ""
    // Failure [MustNotBeEmpty;MustContainAt;MustContainPeriod]

Recommended reading

namespace System
namespace FSharpPlus
namespace FSharpPlus.Data
type VError = | MustNotBeEmpty | MustBeAtLessThanChars of int | MustBeADate | MustBeOlderThan of int | MustBeWithingRange of decimal * decimal
union case VError.MustNotBeEmpty: VError
union case VError.MustBeAtLessThanChars: int -> VError
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
union case VError.MustBeADate: VError
union case VError.MustBeOlderThan: int -> VError
union case VError.MustBeWithingRange: decimal * decimal -> VError
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = Decimal

--------------------
type decimal<'Measure> = decimal
Multiple items
type String = interface IEnumerable<char> interface IEnumerable interface ICloneable interface IComparable interface IComparable<string> interface IConvertible interface IEquatable<string> interface IParsable<string> interface ISpanParsable<string> new: value: nativeptr<char> -> unit + 8 overloads ...
<summary>Represents text as a sequence of UTF-16 code units.</summary>

--------------------
String(value: nativeptr<char>) : String
String(value: char array) : String
String(value: ReadOnlySpan<char>) : String
String(value: nativeptr<sbyte>) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: char array, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
val nonEmpty: x: string -> Validation<VError list,string>
val x: string
Multiple items
val string: value: 'T -> string

--------------------
type string = String
Multiple items
module Validation from FSharpPlus.Data

--------------------
type Validation<'error,'t> = | Failure of 'error | Success of 't static member ( *> ) : x: Validation<'Error,'T> * y: Validation<'Error,'U> -> Validation<'Error,'U> (requires member ``+``) static member (<!>) : f: ('T -> 'U) * x: Validation<'Error,'T> -> Validation<'Error,'U> static member ( <* ) : x: Validation<'Error,'U> * y: Validation<'Error,'T> -> Validation<'Error,'U> (requires member ``+``) static member (<*>) : f: Validation<'Error,('T -> 'U)> * x: Validation<'Error,'T> -> Validation<'Error,'U> (requires member ``+``) static member (<.>) : f: Validation<'Error,('T -> 'U)> * x: Validation<'Error,'T> -> Validation<'Error,'U> (requires member ``+``) static member (<|>) : x: Validation<'Error,'T> * y: Validation<'Error,'T> -> Validation<'Error,'T> (requires member ``<|>``) static member Return: x: 'a -> Validation<'b,'a> static member SequenceBiApply: t: Validation<'Error,'T> list -> Validation<'Error list,'T list> + 1 overload static member get_Empty: unit -> Validation<'a,'b> (requires member Empty)
<summary> A 'Validation' is either a value of the type 'error or 't, similar to 'Result'. However, the 'Applicative' instance for 'Validation' accumulates errors using a 'Semigroup' on 'error. In contrast, the Applicative for 'Result' returns only the first error. A consequence of this is that 'Validation' is not a monad. There is no F#+ 'Bind' method since that would violate monad rules. </summary>
type 'T list = List<'T>
String.IsNullOrEmpty(value: string) : bool
union case Validation.Failure: 'error -> Validation<'error,'t>
union case Validation.Success: 't -> Validation<'error,'t>
val mustBeLessThan: i: int -> x: string -> Validation<VError list,string>
val i: int
val isNull: value: 'T -> bool (requires 'T: null)
property String.Length: int with get
<summary>Gets the number of characters in the current <see cref="T:System.String" /> object.</summary>
<returns>The number of characters in the current string.</returns>
val mustBeWithin: from: decimal * to': decimal -> x: decimal -> Validation<VError list,decimal>
val from: decimal
val to': decimal
val x: decimal
Multiple items
[<Struct>] type DateTime = new: year: int * month: int * day: int -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>

--------------------
DateTime ()
   (+0 other overloads)
DateTime(ticks: int64) : DateTime
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(date: DateOnly, time: TimeOnly) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
   (+0 other overloads)
DateTime(date: DateOnly, time: TimeOnly, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
   (+0 other overloads)
val classicMovie: year: int -> d: DateTime -> Validation<VError list,DateTime>
val year: int
val d: DateTime
property DateTime.Year: int with get
<summary>Gets the year component of the date represented by this instance.</summary>
<returns>The year, between 1 and 9999.</returns>
val date: d: DateTime -> Validation<VError list,DateTime>
property DateTime.Date: DateTime with get
<summary>Gets the date component of this instance.</summary>
<returns>A new object with the same date as this instance, and the time value set to 12:00:00 midnight (00:00:00).</returns>
type Genre = | Classic | PostClassic | Modern | PostModern | Contemporary
union case Genre.Classic: Genre
union case Genre.PostClassic: Genre
union case Genre.Modern: Genre
union case Genre.PostModern: Genre
union case Genre.Contemporary: Genre
Movie.Id: int
Movie.Title: String
Movie.ReleaseDate: DateTime
Movie.Description: String
Movie.Price: decimal
Multiple items
Movie.Genre: Genre

--------------------
type Genre = | Classic | PostClassic | Modern | PostModern | Contemporary
val id: int
val title: string
val releaseDate: DateTime
val description: string
val price: decimal
val genre: Genre
type Movie = { Id: int Title: String ReleaseDate: DateTime Description: String Price: decimal Genre: Genre } static member Create: id: int * title: string * releaseDate: DateTime * description: string * price: decimal * genre: Genre -> Validation<VError list,Movie>
val title: String
val description: String
module Number from Type-validation.MovieValidations
val newRelease: Validation<VError list,Movie>
static member Movie.Create: id: int * title: string * releaseDate: DateTime * description: string * price: decimal * genre: Genre -> Validation<VError list,Movie>
val oldie: Validation<VError list,Movie>
val titleToLong: Validation<VError list,Movie>
String.Concat<'T>(values: Collections.Generic.IEnumerable<'T>) : string
   (+0 other overloads)
String.Concat([<ParamArray>] values: string array) : string
   (+0 other overloads)
String.Concat([<ParamArray>] args: obj array) : string
   (+0 other overloads)
String.Concat(arg0: obj) : string
   (+0 other overloads)
String.Concat(values: Collections.Generic.IEnumerable<string>) : string
   (+0 other overloads)
String.Concat(str0: string, str1: string) : string
   (+0 other overloads)
String.Concat(str0: ReadOnlySpan<char>, str1: ReadOnlySpan<char>) : string
   (+0 other overloads)
String.Concat(arg0: obj, arg1: obj) : string
   (+0 other overloads)
String.Concat(str0: string, str1: string, str2: string) : string
   (+0 other overloads)
String.Concat(str0: ReadOnlySpan<char>, str1: ReadOnlySpan<char>, str2: ReadOnlySpan<char>) : string
   (+0 other overloads)
Multiple items
val seq: sequence: 'T seq -> 'T seq

--------------------
type 'T seq = Collections.Generic.IEnumerable<'T>
Name.unName: String
val s: String
Email.unEmail: String
Age.unAge: int
Person.name: Name
type Name = { unName: String } static member create: s: String -> Name
Person.email: Email
type Email = { unEmail: String } static member create: s: String -> Email
Person.age: Age
type Age = { unAge: int } static member create: i: int -> Age
val name: Name
val email: Email
val age: Age
Multiple items
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>

--------------------
type Error = | NameBetween1And50 | EmailMustContainAtChar | AgeBetween0and120
union case Error.NameBetween1And50: Error
union case Error.EmailMustContainAtChar: Error
union case Error.AgeBetween0and120: Error
val mkName: s: String -> Validation<Error list,Name>
val l: int
val length: source: 'Foldable<'T> -> int (requires member Length)
<summary>Gets the number of elements in the foldable.</summary>
<category index="11">Foldable</category>
<param name="source">The input foldable.</param>
<returns>The length of the foldable.</returns>
static member Name.create: s: String -> Name
val mkEmail: s: string -> Validation<Error list,Email>
val s: string
val contains: char: char -> source: string -> bool
<summary> Does the source string contain the given character? Use `String.isSubstring` to check for strings. </summary>
static member Email.create: s: String -> Email
val mkAge: a: int -> Validation<Error list,Age>
val a: int
static member Age.create: i: int -> Age
val mkPerson: pName: String -> pEmail: string -> pAge: int -> Validation<Error list,Person>
val pName: String
val pEmail: string
val pAge: int
type Person = { name: Name email: Email age: Age } static member create: name: Name -> email: Email -> age: Age -> Person
static member Person.create: name: Name -> email: Email -> age: Age -> Person
val validPerson: Validation<Error list,Person>
val badName: Validation<Error list,Person>
val badEmail: Validation<Error list,Person>
val badAge: Validation<Error list,Person>
val badEverything: Validation<Error list,Person>
module Lens from FSharpPlus
<summary> Lens functions and operators </summary>
val asMaybeGood: Error list option
val _Success: x: ('a -> 'b) -> (Validation<'a,'d> -> 'e) (requires member Map and member Return)
val asMaybeBad: Error list option
val asResultGood: Result<Person,Error list>
val isoValidationResult: x: 'a -> 'b (requires member Dimap and member Map)
val asResultBad: Result<Person,Error list>
module Email from Type-validation
Multiple items
union case AtString.AtString: string -> AtString

--------------------
type AtString = | AtString of string
Multiple items
union case PeriodString.PeriodString: string -> PeriodString

--------------------
type PeriodString = | PeriodString of string
Multiple items
union case NonEmptyString.NonEmptyString: string -> NonEmptyString

--------------------
type NonEmptyString = | NonEmptyString of string
Multiple items
union case Email.Email: string -> Email

--------------------
type Email = | Email of string
type VError = | MustNotBeEmpty | MustContainAt | MustContainPeriod
union case VError.MustContainAt: VError
union case VError.MustContainPeriod: VError
val atString: x: string -> Validation<VError list,AtString>
val periodString: x: string -> Validation<VError list,PeriodString>
val nonEmptyString: x: string -> Validation<VError list,NonEmptyString>
val email: x: string -> Validation<VError list,Email>
val result: x: 'T -> 'Functor<'T> (requires member Return)
<summary> Lifts a value into a Functor. Same as return in Computation Expressions. </summary>
<category index="2">Applicative</category>
val success: Validation<VError list,Email>
val failureAt: Validation<VError list,Email>
val failurePeriod: Validation<VError list,Email>
val failureAll: Validation<VError list,Email>