Simulate polymorphic variants in F#?
There's some interesting proposals for erased type unions, allowing for Typescript-style anonymous union constraints.
type Goose = Goose of int
type Cardinal = Cardinal of int
type Mallard = Mallard of int
// a type abbreviation for an erased anonymous union
type Bird = (Goose | Cardinal | Mallard)
The magic operator which would give you a NameValidationError | EmailValidationError
would have its type exist only at compile-time. It would be erased to object
at runtime.
But it's still on the anvil, so maybe we can still have some readable code by doing the erasing ourselves?
The composition operator could 'erase' (box, really) the result error type:
let (|>>) input validate =
match input with
| Ok(v) -> validate v |> Result.mapError(box)
| Error(e) -> Error(box e)
and we can have a partial active pattern to make type-matching DU cases palatable.
let (|ValidationError|_|) kind = function
| Error(err) when Object.Equals(kind, err) -> Some ()
| _ -> None
Example (with super biased validations):
let person = { Name = "Bob"; Email = "[email protected] "}
let validateName person = Result.Ok(person)
let validateEmail person = Result.Ok(person)
let validateVibe person = Result.Error(NameTooShort)
let result = person |> validateName |>> validateVibe |>> validateEmail
match result with
| ValidationError NameTooShort -> printfn "Why is your name too short"
| ValidationError EmailTooLong -> printfn "That was a long address"
| _ -> ()
This will shunt on validateVibe
This is probably more verbose than you would like but it does allow you to put things into a DU without explicitly defining it.
F# has Choice
types which are defined like this:
type Choice<'T1,'T2> =
| Choice1Of2 of 'T1
| Choice2Of2 of 'T2
type Choice<'T1,'T2,'T3> =
| Choice1Of3 of 'T1
| Choice2Of3 of 'T2
| Choice3Of3 of 'T3
// Going up to ChoiceXOf7
With your existing functions you would use them like this:
// This function returns Result<Person,Choice<NameValidationError,EmailValidationError>>
let validatePerson person =
validateName person
|> Result.mapError Choice1Of2
|> Result.bind (validateEmail >> Result.mapError Choice2Of2)
This is how you would consume the result:
let displayValidationError person =
match person with
| Ok p -> None
| Error (Choice1Of2 NameTooLong) -> Some "Name too long"
| Error (Choice2Of2 EmailTooLong) -> Some "Email too long"
// etc.
If you want to add a third validation into validatePerson
you'll need to switch to Choice<_,_,_>
DU cases, e.g. Choice1Of3
and so on.