Reputation: 37095
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
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 match
s 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
Reputation: 11577
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