How can I flatten nested Results?
Asked Answered
A

3

20

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?

Athwart answered 25/11, 2019 at 9:10 Comment(2)
See also How do I unwrap an arbitrary number of nested Option types?Rowe
Thankfully, this exists now: doc.rust-lang.org/std/result/enum.Result.html#method.flatten. Currently, nightly onlyUnqualified
P
35

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.

Pitsaw answered 25/11, 2019 at 10:13 Comment(1)
Its cool that Result is a propper monad. It makes some things much easier, however, and_then works well for mapping from Ok to Err but not from Err to Ok. it only runs the function on Ok branchesRevivalist
W
1

Combining and_then and using the ? operator, my solution is as follows:

pub fn take_first_three_times(tree: &Tree) -> Result<&Tree, MyAwesomeError> {
    tree.get_branch(0).and_then(|first| {
        first.get_branch(0)?.get_branch(0)
    }).map_err(MyAwesomeError::from)
}

really, this just replaces the internal function with a FnOnce closure

Wait answered 29/12, 2022 at 18:16 Comment(0)
T
1

I personally solved it by making a custom trait that allows me to flatten any result where the error of the inner result can be converted into the outer result.

/// flattens a nested result, converting the inner error into the outer error
pub trait FlattenResult<V, OuterError, InnerError>
where
    InnerError: Into<OuterError>,
    Result<V, OuterError>: From<ResultWrapper<V, InnerError>>,
{
    fn flatten_result(self) -> Result<V, OuterError>;
}

impl<V, OuterError, InnerError> FlattenResult<V, OuterError, InnerError>
    for Result<Result<V, InnerError>, OuterError>
where
    OuterError: From<InnerError>,
{
    fn flatten_result(self) -> Result<V, OuterError> {
        match self {
            Ok(inner) => ResultWrapper(inner).into(),
            Err(err) => Err(err),
        }
    }
}

/// A wrapper around Result, to be able to have the custom From implementation
pub struct ResultWrapper<V, E>(Result<V, E>);

impl<V, InnerError, OuterError> From<ResultWrapper<V, InnerError>>
    for Result<V, OuterError>
where
    OuterError: From<InnerError>,
{
    fn from(value: ResultWrapper<V, InnerError>) -> Self {
        match value.0 {
            Ok(inner) => Ok(inner),
            Err(err) => Err(err.into()),
        }
    }
}

You can see here how to use it

I left out document comments from this example, those can also be found in that same file.

The way this works is quite simple. For any Result<Result<V, InnerError>, OuterError> where InnerError can be converted into OuterError, simply coerce the inner error into the outer error.

The downside to this approach is the fact that you do have to import the trait each time you wanna use flatten_result. Personally I found that fine, but obviously YMMV

Tugboat answered 24/11, 2023 at 14:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.