sdgfsdh
sdgfsdh

Reputation: 37095

"Merging" Discriminated Unions in F#?

Following on from this question, I am having an issue combining differently typed Result types together.

(what follows is a contrived example, not real code)

Suppose I have a function that reads a file:

type ReadFileError = 
| FileNotFound of string

let readFile (path : string) : Result<string, ReadFileError> =
  // --- 8< --- 

And a function that parses it somehow:

type JsonParseError = 
| InvalidStructure of string

let parseJson (content : string) : Result<Json, JsonParseError> = 
  // --- 8< --- 

Now I can combine these to create a function that reads and parses a file:

type ReadJsonError = 
| ReadFileError of ReadFileError
| JsonParseError of JsonParseError

let readJson (path : string) : Result<Json, ReadJsonError> = 
  match path |> readFile with
  | Ok content -> 
    match content |> parseJson with 
    | Ok json -> Ok json
    | Error e -> Error (ReadJsonError.JsonParseError e)
  | Error e -> Error (ReadJsonError.ReadFileError e)

As you can see, unifying the error types is rather awkward. I need to define a new union-type and wrap the Error side properly. This is not something you have to worry about with an exception based approach, since throw is open-ended with regard to types.

Is it possible to make the Result style convenient when combining errors of different types?

Upvotes: 4

Views: 728

Answers (2)

AMieres
AMieres

Reputation: 5004

Short and easy answer first. I'll come back later for a longer answer.

If you are building a monolithic application the suggestion is to create just one Error type for the whole application:

type AllErrors = 
| FileNotFound of string
| InvalidJsonStructure of string
| OtherErrors ...

That will give you one nice place where all errors are defined and you can create a unified printError and other error-handling functions.

Sometimes that is not possible, for instance if your code is modular and each module has its own ErrorType then you have two options, still create a unique type and map to it or create a nested, composed type like you did. It is your decision. In both cases you use Result.mapError

Syntactically there are many ways to do this. To avoid the nested matchs you use Result.bind and Result.mapError

let readJson (path : string) : Result<Json, ReadJsonError> = 
    readFile path 
    |> Result.mapError ReadFileError
    |> Result.bind (fun content ->
    parseJson content 
    |> Result.mapError JsonParseError
    )

If you had a result Computation Expression:

type Builder() =
    member inline this.Return          x       = Ok  x
    member inline this.ReturnFrom      x       =     (x:Result<_,_>)
    member        this.Bind           (w , r ) = Result.bind  r w
    member inline this.Zero           ()       = Ok ()
let result = Builder()

then it would look like this:

let readJson (path : string) : Result<Json, ReadJsonError> = result {
    let! content = readFile  path    |> Result.mapError ReadFileError
    return!        parseJson content |> Result.mapError JsonParseError
}

with operators:

let (>>= ) vr f = Result.bind     f vr
let (|>>.) vr f = Result.mapError f vr

it could be this:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    readFile path     |>>. ReadFileError
    >>= fun content ->
    parseJson content |>>. JsonParseError

or this:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    path 
    |>   readFile  
    |>>. ReadFileError
    >>= fun content ->
    content 
    |>   parseJson 
    |>>. JsonParseError

or even this:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    path           |>
    readFile       |>>. 
    ReadFileError  >>= 
    fun content    ->
    content        |>   
    parseJson      |>>. 
    JsonParseError

Ok, this last one is just for fun. I am not advocating you code like this.

also you could simply create unified version of your functions:

let readFileU = readFile  >> Result.mapError ReadFileError
let readJsonU = parseJson >> Result.mapError JsonParseError

and bind them with the Kleisli operator:

let (>=>) f g p = f p |> Result.bind g

let readJson = readFileU >=> readJsonU

Upvotes: 3

Combining the error types is a problem with Result that I only realized when trying it out.

With exceptions this is "solved" with having all exceptions inherit a base class. So one similar approach could be type R<'T> = Result<'T, exn>

However, I find that unappealing and usually fall into a pattern where I define my own Result type that allows aggregated failures of a homogeneous type.

A bit like this

type BadResult = Message of string | Exception of exn
type BadTree = Leaf of BadResult | Fork of BadTree*BadTree
type R<'T> = Good of 'T | Bad of BadTree

Another approach could be to combine Result failures using Choice. Not sure one would end up into an especially appealing place with this.

let bind (t : Result<'T, 'TE>) (uf  'T -> Result<'U, 'UE>) : Result<'U, Choice<'TE, 'TU>> = ...

This probably don't help you at all but perhaps it spawns a few ideas on how to proceed?

Upvotes: 3

Related Questions