【问题标题】:How to use higher-rank trait bounds to make a returned impl Fn more generic?如何使用更高等级的特征边界使返回的 impl Fn 更通用?
【发布时间】:2020-03-28 18:59:02
【问题描述】:

我最近遇到了 Rust 的高级特征边界,并认为我可以使用它们来使我正在编写的解析器中的一些函数更通用。但是,我所做的修改给了我一条我无法正面或反面的错误消息。

这就是我现在所拥有的有效方法:

use nom::bytes::complete::is_not;
use nom::character::complete::multispace0;
use nom::combinator::verify;
use nom::error::{
    ParseError,
    VerboseError,
};
use nom::sequence::terminated;
use nom::IResult;

fn one_token<'a, E>(input: &'a str) -> IResult<&str, &str, E>
where
    E: ParseError<&'a str>,
{
    terminated(is_not(" \t\r\n"), multispace0)(input)
}

fn str_token<'a, E>(expected_string: String) -> impl Fn(&'a str) -> IResult<&str, &str, E>
where
    E: ParseError<&'a str>,
{
    verify(one_token, move |actual_string| {
        actual_string == expected_string
    })
}

这样编译。但是,我的直觉告诉我,我从str_token 返回的impl Fnstr_token 上的生命周期参数约束并不一定很好。我相信这样可能会不必要地限制返回的 impl Fn 特征的有用性。所以我想我可以修改它以返回一个适用于任何生命周期 'bimpl Fn,而不管工厂函数 str_token_hrtb 的生命周期是什么:

fn str_token_hrtb<'a, E>(
    expected_string: String,
) -> impl for<'b> Fn(&'b str) -> IResult<&str, &str, E>
where
    E: ParseError<&'a str>,
{
    verify(one_token, move |actual_string| {
        actual_string == expected_string
    })
}

现在,编译器给了我这些错误:

error[E0277]: expected a `std::ops::Fn<(&'b str,)>` closure, found `impl std::ops::Fn<(&str,)>`
  --> src/main.rs:29:6
   |
29 | ) -> impl for<'b> Fn(&'b str) -> IResult<&str, &str, E>
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an `Fn<(&'b str,)>` closure, found `impl std::ops::Fn<(&str,)>`
   |
   = help: the trait `for<'b> std::ops::Fn<(&'b str,)>` is not implemented for `impl std::ops::Fn<(&str,)>`
   = note: the return type of a function must have a statically known size

error[E0271]: type mismatch resolving `for<'b> <impl std::ops::Fn<(&str,)> as std::ops::FnOnce<(&'b str,)>>::Output == std::result::Result<(&'b str, &'b str), nom::internal::Err<E>>`
  --> src/main.rs:29:6
   |
29 | ) -> impl for<'b> Fn(&'b str) -> IResult<&str, &str, E>
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected bound lifetime parameter 'b, found concrete lifetime
   |
   = note: the return type of a function must have a statically known size

我不明白如何阅读。是不是说for&lt;'b&gt; std::ops::... 的返回值没有实现特征verify?如果是这样,为什么不呢?为什么str_token 不存在同样的问题?另外,我找不到任何方法来解释第二条type mismatch 错误消息。

谁能提供一些关于我在这里做错了什么以及编译器试图告诉我什么的见解?

更新

我正在使用此处的 nom 解析库:https://github.com/Geal/nom/

另外,verify 函数的代码在这里:https://github.com/Geal/nom/blob/851706460a9311f7bbae8e9b7ee497c7188df0a3/src/combinator/mod.rs#L459

另一个更新

因为我意识到我可能没有提出足够具体的问题,所以决定关闭它。

【问题讨论】:

    标签: rust


    【解决方案1】:

    错误信息确实不太理想。这是因为leak check step 处理 hrtb 生命周期的所有困难尚未解决。 Niko 一直在致力于 it

    如果您通过传递+nightly -Zno-leak-check 标志关闭泄漏检查,您将收到更合理的错误消息:

    error: implementation of `std::ops::FnOnce` is not general enough
       --> src/main.rs:27:6
        |
    27  |   ) -> impl for<'b> Fn(&'b str) -> IResult<&str, &str, E>
        |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough
        |
        = note: `impl std::ops::Fn<(&str,)>` must implement `std::ops::FnOnce<(&str,)>`
        = note: ...but `std::ops::FnOnce<(&'b str,)>` is actually implemented for the type `impl std::ops::Fn<(&str,)>`
    

    现在,当您实际返回 Fn 时,为什么编译器会抱怨 FnOnce?这是我的猜测,但它应该是合理的。 FnOnceFn 的超级特征,rustc 是说代码甚至不满足超级特征,它无法满足更克制的特征本身。

    现在这终于有意义了。考虑verify 被定义为:

    pub fn verify<I: Clone, O1, O2, E: ParseError<I>, F, G>(
        first: F, 
        second: G
    ) -> impl Fn(I) -> IResult<I, O1, E> where
        F: Fn(I) -> IResult<I, O1, E>,
        G: Fn(&O2) -> bool,
        O1: Borrow<O2>,
        O2: ?Sized,
    

    要求它是:

    impl for<'b> Fn(&'b str) -> IResult<&str, &str, E>
    where
        E: ParseError<&'a str>,
    

    你问的是forall&lt;'b&gt; 'b = 'a,这根本不可能。


    了解生命周期在 Rust 中是如何工作的非常有帮助,而且几乎是强制性的。但是,在编写自己的代码时,最好尽可能跳过它们。终生省略规则可以帮助您做到这一点。一个人当然不会自愿达到 hrtb 生命周期:D

    例如,编写one_tokenstr_token 函数的更惯用方式可能是:

    fn one_token(input: &str) -> IResult<&str, &str>
    {
        let res = terminated(is_not(" \t\r\n"), multispace0)(input)?;
    
        Ok(res)
    }
    
    fn str_token(input: &str, expected_string: String) -> IResult<&str, &str>
    {
        let res = verify(one_token, |actual_string| {
            actual_string == expected_string
        })(input)?;
    
        Ok(res)
    }
    

    【讨论】:

    • 我遇到对 HRTB 需求的不仅仅是这个例子。我确实在一定程度上理解了生命周期,但坦率地说,这个特性及其所有细微差别根本没有得到很好的记录。例如,我收集到在函数上指定为泛型类型参数的生命周期变量通常绑定在函数的调用站点。但是,绑定行为似乎也取决于函数参数中的第一个变量或使用生命周期的返回值。
    • 比如str_token,不知道lifetime参数是绑定在str_token的调用点还是返回的impl Fn的调用点。对我来说,它将绑定在返回的 impl Fn 的调用站点上更有意义,但是文档在哪里阐明这实际上是发生了什么?
    • 另外,您提供的那些示例由于几个原因无法正常工作。首先,nom 库通常将解析器定义为始终接受一个参数的函数,该参数是要解析的输入。所以你定义的str_token不能和nom一起使用。另外,我希望我的解析器允许调用者在解析器的调用站点使用 turbofish 语法指定错误类型。这是nom 库的作者推荐的惯用模式,我想坚持下去。这需要错误类型参数以及它包装的输入类型的显式生命周期。
    猜你喜欢
    • 2020-12-30
    • 2021-04-15
    • 2021-10-20
    • 2018-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多