Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I flatten nested Results?

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?

like image 494
MartenCatcher Avatar asked Nov 25 '19 09:11

MartenCatcher


1 Answers

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.

like image 145
Yuri Kovalenko Avatar answered Nov 03 '22 12:11

Yuri Kovalenko