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?
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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With