Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulate polymorphic variants in F#?

I'm new to F# so forgive me in advance if this is a stupid question or if the syntax may be a bit off. Hopefully it's possible to understand the gist of the question anyways.

What I'd like to achieve is the possibility to compose e.g. Result's (or an Either or something similar) having different error types (discriminated unions) without creating an explicit discriminated union that includes the union of the two other discriminated unions.

Let me present an example.

Let's say I have a type Person defined like this:

type Person =
    { Name: string
      Email: string }

Imagine that you have a function that validates the name:

type NameValidationError = 
  | NameTooLong
  | NameTooShort

let validateName person : Result<Person, NameValidationError>

and another that validates an email address:

type EmailValidationError = 
  | EmailTooLong
  | EmailTooShort

let validateEmail person : Result<Person, EmailValidationError>

Now I want to compose validateName and validateEmail, but the problem is that the error type in the Result has different types. What I'd like to achieve is a function (or operator) that allows me to do something like this:

let validatedPerson = person |> validateName |>>> validateEmail

(|>>> is the "magic operator")

By using |>>> the error type of validatedPerson would be a union of NameValidationError and EmailValidationError:

Result<Person, NameValidationError | EmailValidationError>

Just to make it clear, it should be possible to an use arbitrary number of functions in the composition chain, i.e.:

let validatedPerson : Result<Person, NameValidationError | EmailValidationError | XValidationError | YValidationError> = 
       person |> validateName |>>> validateEmail |>>> validateX |>>> validateY

In languages like ReasonML you can use something called polymorphic variants but this is not available in F# as afaict.

Would it be possible to somehow mimic polymorphic variants using generics with union types (or any other technique)?! Or is this impossible?

like image 544
Johan Avatar asked Mar 08 '20 10:03

Johan


2 Answers

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

like image 87
Asti Avatar answered Nov 08 '22 22:11

Asti


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.

like image 2
TheQuickBrownFox Avatar answered Nov 08 '22 22:11

TheQuickBrownFox