Rust异步借用生命周期

4
我正在尝试制作一个帮助程序,允许异步链接副作用,但我无法正确地得到通用约束条件,以便编译器理解future的输出超过了用于构建它的引用。
这主要是由于以下代码导致的: Playground链接
问题的核心在于:
struct Chain<T> {
    data: T
}
impl<T> Chain<T> {
    pub async fn chain<E, Fut, F>(self, effect: F) -> Result<T, E>
        where
            Fut: Future<Output=Result<(), E>>,
            F: FnOnce(&T) -> Fut
    {
        todo!()
    }
}

会导致编译错误

error: lifetime may not live long enough
  --> src/main.rs:39:32
   |
39 |     let r = chain.chain(|this| this.good("bar")).await;
   |                          ----- ^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                          |   |
   |                          |   return type of closure `impl Future` contains a lifetime `'2`
   |                          has type `&'1 MyData`

如果我们修复chain,以便它可以推断引用与未来具有相同的生命周期:
impl<T> Chain<T> {
    pub async fn chain<'a, E, Fut, F>(self, effect: F) -> Result<T, E>
        where
            T: 'a, 
            Fut: 'a + Future<Output=Result<(), E>>,
            F: FnOnce(&'a T) -> Fut
    {
        effect(&self.data).await?;
        Ok(self.data)
    }
}

我们得到了一个新的编译器错误,关于在借用 self.data 的同时移动它。
error[E0505]: cannot move out of `self.data` because it is borrowed
  --> src/main.rs:30:12
   |
23 |     pub async fn chain<'a, E, Fut, F>(self, effect: F) -> Result<T, E>
   |                        -- lifetime `'a` defined here
...
29 |         effect(&self.data).await?;
   |         ------------------
   |         |      |
   |         |      borrow of `self.data` occurs here
   |         argument requires that `self.data` is borrowed for `'a`
30 |         Ok(self.data)
   |            ^^^^^^^^^ move out of `self.data` occurs here

我猜想在 |this| futures::future::ready(Err(this)) 这一行存在一种病理性的闭包,会导致提前返回但借用仍然“存活”。

问题

我们如何让 chain 起作用? 我通常使用的块作用域生命周期技巧似乎无法帮助解决这个问题。 是否有一组可以添加到where约束条件中以证明借用和最终移动位于不相交的生命周期上的约束条件?

2个回答

4
这种特殊情况下,当前的约束语法和缺乏高等种类类型使你无法表达所需内容。
可以使用更高秩的 trait 约束for<'a> 语法,在 where 子句中引入一个中间泛型生命周期参数 'a,以指示约束必须对任何生命周期有效。这在此处是必要的,而你的第一个修复方法之所以不起作用,是因为 chain 上的泛型 'a 意味着生命周期由调用者确定,然而,self 的生命周期由构造确定,小于调用者可能选择的任何生命周期。因此,稍微更正确的语法(与去糖代码相同)应该是:
pub async fn chain<E, Fut, F>(self, effect: F) -> Result<T, E>
    where
        Fut: Future<Output = Result<(), E>>,
        F: for<'a> FnOnce(&'a T) -> Fut
{
    ...

但这并没有什么帮助,因为Fut'a之间仍然没有关联。不幸的是,无法在多个约束条件中使用相同的for<'a>。您可以尝试使用impl Trait一次性定义所有内容,但这是不支持的:
pub async fn chain<E, F>(self, effect: F) -> Result<T, E>
    where F: for<'a> FnOnce(&'a T) -> (impl Future<Output = Result<(), E>> + 'a)
{
    ...

error[E0562]: `impl Trait` not allowed outside of function and method return types
  --> src/lib.rs:35:44
   |
35 |         where F: for<'a> FnOnce(&'a T) -> (impl Future<Output = Result<(), E>> + 'a)
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

希望将来能更好地支持高阶类型。这种特殊情况可能会在夜间使用几乎完整的通用关联类型功能得到解决,但我还没有找到。因此,唯一真正的解决方法是使用命名类型作为返回值,这实际上只留下了trait对象。
use std::pin::Pin;
use futures::future::FutureExt;

pub async fn chain<E, F>(self, effect: F) -> Result<T, E>
    where F: for<'a> FnOnce(&'a T) -> Pin<Box<dyn Future<Output = Result<(), E>> + 'a>>
{
    ...

let r = chain.chain(|this| this.good("bar").boxed()).await;

作为一则附注,你的bad情况仍然无法编译,并且确实无法工作,因为你将返回对本地值的引用。

嘿@kmdreko - 感谢您的回复!这句话“self的生命周期构造小于调用者可以选择的任何生命周期”很好,我学到了新东西。我从另一个部分得到了灵感:“不幸的是,无法在多个约束条件中使用相同的for<'a>。”也许可以通过帮助trait将多个限制嵌套到单个约束中?Playground对于为什么助手不够通用,有什么见解吗? - moatra
@moatra,你的坚持终于得到了回报!你的尝试揭示了这个问题:HRTBs:“实现不够通用”,但实际上是,它记录了如果泛型闭包生命周期不能立即推导为Fn*类型(闭包-> FnHelper -> FnOnce),则其并不真正起作用,但对于函数来说却可以!这个可以工作并编译。我将不得不重新考虑和修订我的答案。 - kmdreko
不错的发现!知道可以使用正确注释的函数完成这个任务真是太好了,但如果能够使用闭包并让编译器处理推断就更好了。再次感谢您的帮助! - moatra

0

看起来你正在尝试实现future.then()

如果你已经意识到这一点并且正在练习,那么你可能应该设计一个方式,使effect方法返回值,并使用这些值从chain方法返回。这样可以强制执行正确的操作顺序。就我所理解的你的设计而言,你不会从在chain方法内等待effect中获益,因为你的skip函数也是异步的,并且将返回future(由于异步工作方式如此:它将显式返回类型包装在future中,因此chain方法的实际返回类型为Future<Output=Result<T, E>>)。

因此,在chain内等待effect没有任何意义,每当你使用它时仍然必须await它 - 直到你在chain之外实际等待它才会发生任何事情 - futures以这种方式进行惰性处理。

简而言之,我建议您将效果方法改为返回值,并将链式调用仅返回这些值。


嘿@Scherzo - 感谢你的帮助!future.then()链接是一个很好的提醒,我需要去熟悉一下那个库。在一般情况下,你所说的在链中等待未来通常是无益的。但在我的特定情况下,我需要持久化计算结果并将相同的结果返回给客户端。然而,如果持久化失败,我需要返回一个错误响应给客户端。我会仔细阅读这些文档,看看是否有任何突破口,但我很享受这个过程。 - moatra
@moatra 你提出的".await?"的结果解包了一个值,但它也是一个结果。为什么?因为异步函数(比如你调用的effect)返回一个你提供的值的结果类型:因此effect实际上返回的是Result<Result<T,E2>,E1>类型。如果你说你想发送有关失败计算的错误,你仍然需要处理它——目前你只处理了await/async本身的潜在错误。 - Scherzo
游乐场,展示我想要完成的内容。请查看32和168行的注释。 - moatra

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