Accumulating Results in Rust With Validated
Rust comes out of the box with a Result<T, E>
type in its standard library. For those not familiar with it, it is a union-like enum type where T
is a type parameter denoting the kind object held in a Result
in the success case (Result::Ok<T>
), and E
is a type paramter denoting the kind of error object held in the failure case (Result::Err<E>
). In Scala, this is represented in the standard library as Either[+A, +B]
, where the the success and error type params are swapped (traditionally, the one on the left stands for error and the one on the right is…well, right).
By default, Result
comes with really good support for what I call “early return on error”. That is, you can use map
, and_then
(flatMap in some other languages) to transform them, and if there’s an error at an intermediate step, the chain returns early with a Result::Err<E>
:
1 2 3 4 5 6 |
|
But .. what happens when you have multiple Result
s that are independent of each other, and you want to accumulate not only their collective success case, but also all their collective errors in the failure case?
Let’s have a look at Validated in Frunk (which is itself inspired by Validated
in Cats)
Frunk is published to Crates.io, so to begin, add the crate to your list of dependencies:
1 2 |
|
By the way, to take a dive into the deep end, jump straight to Validated’s Rustdocs.
Imports
Next, let’s add a few imports.
1 2 |
|
Scenario
Suppose we have a Person
struct defined as follows:
1 2 3 4 5 6 |
|
And, we have 3 methods that produce age, name and email for us, but all could potentially fail with a Nope
error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
In real life, these methods would probably be taking an HTML form as an argument and doing some kind of parsing/validation or making calls to a service somewhere, but for simplicity, in our example, each of them takes a single argument that will let us toggle between the success and error cases.
Using Validated
Having set all that up, actually using Validated
to accumulate our Results
is actually very simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
As you can see, all we need to do is call into_validated()
on a given Result
to kick off the validation context, and use +
to add subsequent Result
s into it. At the end, you call into_result()
on the Validated
to turn it back into a Result
and map on the HList
that is contained inside. Inside the lambda, we destructure the HList
using the hlist_pat!
macro, and then instantiate our Person
.
Oh, in case it isn’t obvious, the hlist
passed to the lambda when we map is statically typed in the order that your Result
s were added into the Validated
context, so your code is completely type safe. If you want to learn more about HLists in Frunk, check out this blog post.
Having said that, perhaps in the success case, not much has really changed in comparison to using naked Result
s. That is, you could have gotten here simply by chaining with map
and/or and_then
. But take a look at what happens when one or more of these fail:
1 2 3 4 5 6 7 8 9 |
|
As you can see, the failure case is more interesting because Validated
gives us the ability to accumulate all errors cleanly. For operations like parsing user input or checking parameters passed into our API, this non-early-abort behaviour is highly desirable compared with telling the user what went wrong One. Thing. At. At. Time.
Oh, Validated
s can also be appended to each other:
1 2 3 4 5 6 7 8 |
|
Conclusion
Please take Validated
out for a spin and send suggestions, comments, PRs ! I’ve found this abstraction to be helpful in the Scala world so I’m eager to hear impressions from Rustaceans.