I'm working with a third-party library that provides tree-based data structures that I have to use "as is". The API returns Result<T, Error>
. I have to make some sequential calls and convert the error to my application's internal error.
use std::error::Error;
use std::fmt;
pub struct Tree {
branches: Vec<Tree>,
}
impl Tree {
pub fn new(branches: Vec<Tree>) -> Self {
Tree { branches }
}
pub fn get_branch(&self, id: usize) -> Result<&Tree, TreeError> {
self.branches.get(id).ok_or(TreeError {
description: "not found".to_string(),
})
}
}
#[derive(Debug)]
pub struct TreeError {
description: String,
}
impl Error for TreeError {
fn description(&self) -> &str {
self.description.as_str()
}
}
impl fmt::Display for TreeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.description.fmt(f)
}
}
#[derive(Debug)]
pub struct MyAwesomeError {
description: String,
}
impl MyAwesomeError {
pub fn from<T: fmt::Debug>(t: T) -> Self {
MyAwesomeError {
description: format!("{:?}", t),
}
}
}
impl Error for MyAwesomeError {
fn description(&self) -> &str {
&self.description
}
}
impl fmt::Display for MyAwesomeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.description.fmt(f)
}
}
If I write this code:
pub fn take_first_three_times(tree: &Tree) -> Result<&Tree, MyAwesomeError> {
let result = tree
.get_branch(0)
.map(|r| r.get_branch(0))
.map(|r| r.map(|r| r.get_branch(0)));
// ...
}
The type of result
will be Result<Result<Result<Tree, TreeError>, TreeError>, TreeError>
. I don't want to process errors by cascades of match
.
I can write an internal function that adjusts the API's interface and processes the error in the level of base function:
fn take_first_three_times_internal(tree: &Tree) -> Result<&Tree, TreeError> {
tree.get_branch(0)?.get_branch(0)?.get_branch(0)
}
pub fn take_first_three_times(tree: &Tree) -> Result<&Tree, MyAwesomeError> {
take_first_three_times_internal(tree).map_err(MyAwesomeError::from)
}
How can I achieve this without an additional function?
This is an example of issue, when you're working with various wrappers like Option
in functional programming. In functional programming there are such called 'pure' functions, that instead of changing some state (global variables, out parameters) only rely on input parameters and only return their result as return value without any side effects. It makes programs much more predictable and safe, but it introduced some inconveniences.
Imagine we have let x = Some(2)
and some function f(x: i32) -> Option<f32>
. When you use map
to apply that f
to x
, you'll get nested Option<Option<f32>>
, which is the same issue that you got.
But in the world of functional programming (Rust was inspired with their ideas a lot and supports a lot of typical 'functional' features) they came up with solution: monads.
We can show map
a signature like (A<T>, FnOnce(T)->U) -> A<U>
where A
is something like wrapper type, such as Option
or Result
. In FP such types are called functors. But there is an advanced version of it, called a monad. It has, besides the map
function, one more similar function in its interface, traditionally called bind
with signature like (A<T>, FnOnce(T) -> A<U>) -> A<U>
. More details there.
In fact, Rust's Option
and Result
is not only a functor, but a monad too. That bind
in our case is implemented as and_then
method. For example, you could use it in our example like this: x.and_then(f)
, and get simple Option<f32>
as a result. So instead of a .map
chain you could have .and_then
chain that will act very similar, but there will be no nested results.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With