Either Types for Rust
RustFunctional ProgrammingExceptionsEitherI’ve written extensively about the Either datatype this year. It’s an excellent way to model errors without resorting to exceptions.
Kotlin has been thus far my go-to language to show this concept. However, I’ve been playing a lot with Rust lately. My first instinct was to explore its functional programming capabilities. Rust really delivers there.
There are plenty of exciting ideas in Rust. The borrow checker, lifecycles, and plenty of others. I’m going to focus on Either in this post, although you might be more familiar with its actual name in the language, Result.
A reason to use Rust
Rust is an increasingly popular language. It’s defined as:
A language empowering everyone to build reliable and efficient software.
Is Rust a replacement for, say, Kotlin or Ruby? I don’t think so! A systems programming language like Rust might not be the tool you want to use to write applications, especially if there is no need to have tight memory management.
However, I’ve found a use case where Rust fits very well: CLI Tools. By that, I mean tools like cat
that are used from the command line. Typically, they start as shell scripts that inevitably get too hard to read and maintain. With Rust, you can use a pretty high-level language, and package it as a binary that’s extremely easy to distribute, be it in a Docker container or anywhere else.
Go has seen success in this space (envconsul comes to mind). After having been involved with both, I find Rust much more pleasant to use.
Either is actually called Result in Rust
Just in case you missed previous posts, let’s quickly present Either. Either is an entity whose value can be of two different types, called left and right. It represents a computation that can fail. So the Right
side is the result in case of success, and the Left
one is the error if something goes wrong.
It turns out that Rust has a built-in Either datatype, called Result. It has two possible values, Ok
and Err
. Rust doesn’t have exceptions, which means that handling error conditions is predominantly done using Result. Great news!
Using Result
Result integrates well into the language. It’s implemented as an enumeration with two possible types.
enum Result<T, E> {
Ok(T),
Err(E),
}
Being an enumeration comes in handy when we try to unwrap the value, as we’ll see in a second.
Returning a Result
Any value can be wrapped using Ok
and Err
as constructors.
let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("failed :(");
Unwrapping a value
Many operations in Rust return a Result. How do you get the value inside? How do you decide what to do based on the outcome of the operation? The answer to both questions lies in pattern matching.
fn read_state(file: Result<i32,&str>) {
match file {
Ok(answer) => println!("Extracted from file {}", answer),
Err() => println!("Bitter disappointment"),
}
}
Note how we can decide what to do based on the Result type and extract our data in one operation. That’s convenient. Moreover, pattern matching is exhaustive, so we’re sure we’ve handled every possibility. I love pattern matching.
Chaining
You rarely do just one computation. So, are you expected to unwrap the value, transform it, and rewrap it every time? That’d be very annoying. Instead, let’s use map
and flatMap
(called and_then
in Rust) to apply a function and get a new Result. If you remember, these methods are biased, so they’ll never be applied to an error case.
pub fn from_file(file_name: &str) -> anyhow::Result<Self> {
fs::read_to_string(file_name)
.and_then(|content| serde_json::from_str(&content))
}
Flat syntax
One disadvantage of modeling errors with Result/Either is that your code can become quite nested as you operate on the data contained within it. The do notation, coming from Haskell, attempts to address this, though it’s not without detractors.
Luckily, Rust has a solution for that as well! The question mark operator. Using it, you can extract the data from an Ok result or return directly the Err if it didn’t work. It looks like this:
pub fn from_file(file_name: &str) -> anyhow::Result<Self> {
let content = fs::read_to_string(file_name)?;
let result = serde_json::from_str(&content)?;
Ok(result)
}
Both read_to_string
and from_str
can fail, thus returning a Result
. If that happens, computations stop, and the method will return an error. The code remains readable without compromising its security.
It combines well with the anyhow crate. If you have multiple kinds of errors in your method, anyhow helps with the propagation. If they implement std::error::Error
, that is.
Conclusion
Who said you couldn’t use functional programming concepts in a systems programming language like Rust? I’ve been reading a lot of Go code lately, and this code feels so much more readable than the endless list of if err != nil
spread through it. If only Kubernetes stuff would be written in Rust!