Simulate polymorphic variants in F#?

Johan :

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?

Asti :

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=368803&siteId=1