Results are a kind of sum type, an umbrella under algebraic data types. They are states, which is the sum of the cardinality of their Ok
and Error
variants. For example:
is a 3-sum type, as there are 2 possible states for it’s okay variant, and one for the Error variant.
This is isomorphic (same “shape” / properties) to Option<bool>
.
Option
vsResult
Result<T, E>
can be thought of as a more generalized version ofOption<T>
, when|E|
has a cardinality of 1.1
Thinking of them as the same helps to simplify the mental model—on a high-level, they are one and the same.
Details about the ?
Operator
Haskell has a do
statement that maps the “successful” branch of fallible computations.
Rust has the ?
which achieves this same feature in Haskell, but is more powerful because it’s an expression as opposed to a statement. This means that it can be used in along with or nested in other expressions.
It is somewhat equivalent to:
The ?
operator on a Result<T, E>
type, is of type T
. This is because the error branch diverges from the main code. In fact, the error branch has the [[The Never !
Type|type !
]] (pronounced “never”), and is a Bottom type in Rust.
Using Option
or Result
in Practice
Single Failure Mode
For situations where a code can fail in only one way, it’s your choice on whether you want to return Option
or Result
. A reddit post (shown in the video) seems to suggests that the official docs suggests to use Result
type in such situations, even though some official libraries don’t practice it either.
The way the author thinks about it is that if there’s only one obvious failure mode, then Option
suffices.
Examples include retrieving results from a dictionary, where the only failure mode that’s intuitive to the API caller is equivalent to KeyError
in Python.
The benefit of using Option
is that it keeps the API cleaner, instead of Result<T, E>
where the Error type has to also be specified.
Multiple Failure Modes
When there’s multiple failure modes, it’s likely better to use Result
as it can provide more information to the caller.
The caller can then decide how they want to handle the error in their code, or not at all and simply panic
or bundle up their error types. However, if we as the implementor do not provide it, then there’s no way at all for the caller to distinguish between failure types, which makes for a poor API.
Bubbling Up Multiple Distinct Failure Types
Libraries
[[thiserror
]] vs [[anyhow
]]
The Infallible
Enum
Sometimes, you may be writing code that can never fail, for a trait that expects an Error trait. In such a case, using ()
works but is semantically incorrect.
This is because ()
implies that there is one failure state, when in reality there are no failure states.
For this, we can use
for which there are exactly zero states, to imply that it cannot ever fail.
Going back to the previous example, Result<bool, Infallible>
removes the Result
type, effectively making it isomorphic to bool
.
Footnotes
-
A more recent reading from Rust for Rustaceans points to this not being semantically equal.
Result
contains the usage of#[must_use]
. ↩