如何将嵌套的结果展开?

20

我正在使用一个第三方库,该库提供基于树形结构的数据结构,我必须按原样使用。该API返回Result<T, 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)
    }
}

如果我写下这段代码:
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)));
    //    ...
}
result的类型将是Result<Result<Result<Tree, TreeError>, TreeError>, TreeError>。我不想通过一系列match来处理错误。
我可以编写一个内部函数,调整API的接口并在基本函数层面处理错误:
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)
}

如何在不使用额外函数的情况下实现这个?

4
感谢上天,现在这个函数终于存在了:https://doc.rust-lang.org/std/result/enum.Result.html#method.flatten。目前只支持夜版。 - BallpointBen
3个回答

34
这是一个问题的例子,当你在使用函数式编程中的各种包装器,如Option时。在函数式编程中有所谓的“纯”函数,它们不改变任何状态(全局变量、输出参数),只依赖于输入参数并将其结果作为返回值返回,不产生任何副作用。这使得程序更加可预测和安全,但也带来了一些不便。
假设我们有let x = Some(2)和一些函数f(x: i32) -> Option<f32>。当您使用mapf应用于x时,您会得到嵌套的Option<Option<f32>>,这与您遇到的问题相同。
但在函数式编程的世界中(Rust受到他们的思想很多启发,并支持很多典型的“函数式”特性),他们提出了解决方案:单子。
我们可以展示map一个签名(A<T>, FnOnce(T)->U) -> A<U>,其中A是像OptionResult这样的包装类型。在FP中,这些类型被称为函子。但是还有一个高级版本,称为单子。它除了map函数之外,在其接口中还有一个类似的函数,传统上称为bind,其签名为(A<T>, FnOnce(T) -> A<U>) -> A<U>。更多细节请参见这里
事实上,Rust的OptionResult不仅是函子,也是单子。在我们的例子中,bind被实现为and_then方法。例如,您可以像这样在我们的示例中使用它:x.and_then(f),并得到简单的Option<f32>作为结果。因此,您可以拥有.and_then链而不是.map链,它将非常相似,但没有嵌套的结果。

很酷的是Result是一个合适的单子。它使得一些事情变得更容易,然而,and_then对于从OkErr的映射效果很好,但对于从ErrOk的映射效果不佳。它只在Ok分支上运行函数。 - undefined

1

结合使用and_then?运算符,我的解决方案如下:

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)
}

实际上,这只是用 FnOnce 闭包替换了内部函数


0
我个人通过创建一个自定义特征来解决这个问题,该特征允许我将内部结果的错误转换为外部结果并将其展平。
/// 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()),
        }
    }
}

你可以在这里看到如何使用它。
在这个例子中,我省略了文档注释,这些注释也可以在同一个文件中找到。
这个工作原理非常简单。对于任何 `Result, OuterError>`,其中 `InnerError` 可以转换为 `OuterError`,只需将内部错误强制转换为外部错误。
这种方法的缺点是每次想要使用 `flatten_result` 时都必须导入该特性。个人而言,我觉得这样做没问题,但显然因人而异。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接