8

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?

Johan
  • 37,479
  • 32
  • 149
  • 237
  • This is interesting. Basically like monadic bind for Either (e.g. [here](https://www.scala-lang.org/api/current/scala/util/Try.html#flatMap[U](f:T=%3Escala.util.Try[U]):scala.util.Try[U])), but with an untagged union type in the error field. But I don't really know F#, sorry... – phipsgabler Mar 08 '20 at 16:56

2 Answers2

3

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 = "bob@email.com "}
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

Asti
  • 12,447
  • 29
  • 38
  • This, to me, looks like a really interesting workaround :). However, if I understand it correctly, I would need to remember to handle all errors myself? Ideally I would like the compiler to _force_ me to handle all errors. – Johan Mar 08 '20 at 17:43
  • 1
    @Johan I hope they implement that anonymous DU proposal :) Then we could have compiler type check all of types in the anonymous union. But there're truly horrifying things you can do with SRTP. I hope someone else posts a more interesting answer. – Asti Mar 08 '20 at 17:54
  • What's meant by SRTP? – Johan Mar 08 '20 at 18:12
  • 1
    Statically Resolved Type Parameters [SRTP](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters). See [this answer](https://stackoverflow.com/questions/60204502/summing-over-lists-of-arbitrary-levels-of-nestedness-in-f) for a nested list sum implementation resolved at compile-time. – Asti Mar 08 '20 at 18:32
2

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.

TheQuickBrownFox
  • 10,544
  • 1
  • 22
  • 35
  • One of pitfalls of this is the resulting type explosion when you have half-a-dozen DUs to cover. There's a good discussion over in #538 and related issues. – Asti Mar 13 '20 at 09:10
  • @Asti Yeah I would love to try polymorphic variants but I've never used them so I haven't felt the possible downside of trying to understand the more complicated type errors that would result in a larger codebase. – TheQuickBrownFox Mar 13 '20 at 10:54
  • If you've used Typescript, you may have already used them in one form or other. Typescript implements structural-typing which is very interesting in its own right. – Asti Mar 13 '20 at 11:49