Today's focus is how to work with F#'s Result, which is a type I personally reach for very often.
Once you are familiar with Result it it is a joy to use, but sometimes you find yourself in a situation where you are unsure how to get from where you are to where you want to be.
Results, Choices, Exceptions
The Result type is a bit opinionated. In contrast to the Choice
type's cases which are numbered, or Haskell's Either
which has left and right, the naming scheme of Ok
and Error
makes it suited to processes where there is an obvious "happy path" and "sad path". Sometimes a sad path can be confused with an exceptional path. In my opinion they are separate!
- The happy path, represented by
Ok
, is what you are hoping will happen. - The sad path, represented by
Error
, is what you were hoping would not happen - but it is not unexpected. - The exceptional path, represented by a raised exception is typically something which is outside the scope of the domain we are operating in - such as an IP being unreachable or database being down (both are probably exceptional, unless those are your domain!).
Sometimes APIs - especially those not created using F# - are created in a way where all sad paths are exceptions (!), and in such cases it is totally fine to manually convert to a Result.Error
instead.
Here's a short - slightly contrived - example which does some local validation on some data before attempting to store it in a database:
let validateData string: Result<Data, DataError list> =
...
let saveData database string: unit =
// throws PrimaryKeyViolationException if the data is already present
// throw ConnectionException if a connection to the DB cannot be made
...
let saveInput rawInput =
match validateData rawInput with
| Ok validatedData ->
try
Ok (saveData database validatedData)
with
| :? PrimaryKeyViolationException -> Error UserAlreadyExists
| :? ConnectionException -> reraise ()
| Error e -> Error e
Here, we use try ... with
to catch the PrimaryKeyViolationException
and map it into our error domain, and reraise
the connection exception to keep it as an exception.
Working with multiple results
Let's take a look at a different example, this one contains multiple results.
Note: In this example I've opted to use the result
computation expression for syntactical clarity. This is functionally equivalent to nested matching into Ok
branches, or chaining Result.bind
between each let!
expressions. Check out Isaac's post on computation expressions to learn a bit more, including how they can be used to create domain specific languages.
let validateEmail: Result<Email, EmailError> = ...
let validatePassword: Result<Password, PasswordError> = ...
type User = { Email: Email; Password: Password }
let validateUser form =
result {
let! email = validateEmail form.Email
let! password = validatePassword form.Password
return { Email = email; Password = password }
}
There are some things to note:
1) The error type of the validate functions do not match, this code will not compile!
1) If there is an error it will return immediately. So if the email and the password are both invalid, the user won't know about any password errors until the email is fixed.
Let's address these points in order.
Uniting error types
When first encountered, figuring out how to join up different types may not be obvious. Some developers will use an error type of string. That's a shame as, in contrast to discriminated unions, strings are not very well suited to pattern matching, so by converting them at this point we lose a lot of flexibility.
A better way would be to create a type which unites the disparate error types.
The perfect type for uniting types is... a union type. (surprise!) Let's give it a go, all we need to do is:
- Create a DU which has a case for each type of error we need to unite.
- Pipe different results into
Result.mapError
and map to the matching case.
[<RequireQualifiedAccess>]
type UserValidationError =
| Email of EmailError
| Password of PasswordError
let validateUser form =
result {
let! email =
validateEmail form.Email
|> Result.mapError UserValidationError.Email
let! password =
validatePassword form.Password
|> Result.mapError UserValidationError.Password
return { Email = email; Password = password }
}
Now this code will compile, and we can pattern match out any specific errors as needed. More on that later.
Returning more than one error
Now onto the second point - what about when we want to see more errors than just the first one we encounter? In this case we want to figure out ALL the errors, and then return.
We now need to mix in some kind of collection since we will be returning potentially multiple errors.
let validateUser form =
let email =
validateEmail form.Email
|> Result.mapError UserValidationError.Email
let password =
validatePassword form.Password
|> Result.mapError UserValidationError.Password
match email, password with
| Ok email, Ok password -> Ok { Email = email; Password = password }
| Error email, Ok _ -> Error [ email ]
| Ok _, Error password -> Error [ password ]
| Error email, Error password -> Error [ email; password ]
A bunch of repetitive and error-prone boilerplate, how horrible!
Also, notice that we have removed the result
CE, and switched let!
to simple let
bindings, that's why we have to do all the manual type unwrapping in the match
statement at the end. This is to avoid the default behaviour of result
which would exit as soon as any error is encountered.
Fortunately, we can lean on the FsToolkit.ErrorHandling library to make this a less painful experience.
Let's try out the validation
computation expression, with the and!
keyword. When using and!
we get back all the errors if there are any, not the first one.
let validateUser form =
validation {
let! email =
validateEmail form.Email
|> Result.mapError UserValidationError.Email
and! password =
validatePassword form.Password
|> Result.mapError UserValidationError.Password
return { Email = email; Password = password }
}
Well, that's a lot more manageable.
This block of code returns a Validation<User, UserValidationError>
, which is a type alias for Result<User, UserValidationError list>
- the same as the return type of the previous example, but a lot easier to write.
Processing error lists
We've looked at how to get errors unified, and now we're ending up with lists of errors. The next question is usually something like this: "how do we extract the base error types from the unified list?"
I tend to write little helper functions to match out each case of the combined DU. We can then feed those into List.choose
to get out a list of the errors we care about.
module UserValidationError =
let tryEmail =
function
| UserValidationError.Email x -> Some x
| _ -> None
let tryPassword =
function
| UserValidationError.Password x -> Some x
| _ -> None
match validateUser form with
| Ok user ->
// Do something with the validated User
| Error errorList ->
// Tell the user about the validation errors somehow!
let emailErrors = errorList |> List.choose UserValidationError.tryEmail
let passwordErrors = errorList |> List.choose UserValidationError.tryPassword
Conclusion
Hopefully you have found this review of some of the slightly more advanced result techniques useful.
For more on this style of programming by creating a pipeline of results, check out this F# for Fun and Profit series: Railway-oriented programming which I would highly recommend. Don't skip the follow up post which talks about when this style is not suitable!
For more on the validation type from FsToolkit, check Ryan's post Validation with F# 5 and FSToolkit.
Finally, I would be remiss not to point out that not everyone agrees on how to use results! For some different views on F#s Result type, check out these posts:
- Don't rely on results by Eirik Tsarpalis
- Fault Report by Paul Blasucci