Specifying lifetimes more granually

Hello,

I'm trying to understand how to define the lifetime bounds of a rust function that takes an "async closure" (a normal closure that returns a future). I can get my ideal syntax working with actual async closures on nightly, but no luck replicating this on stable.

pub async fn repeat_new<F, Fut, T>(&mut self, mut cb: F) -> T
where
    // Seems to be the crux
    for<'a> F: FnMut(&'a mut Ctx) -> Fut + 'a,
    Fut: Future<Output = Loop<T>> + Send,
{
    loop {
        let mut branch = self.clone();

        match cb(&mut branch).await {
            Loop::Continue => {}
            Loop::Break(v) => break v,
        }

        branch.ack().await;
    }
}

// Works great
pub async fn repeat_nightly<F, T>(&mut self, mut cb: F) -> T
where
    F: AsyncFnMut(&mut Ctx) -> Loop<T>,
{
    loop {
        let mut branch = self.clone();

        match cb(&mut branch).await {
            Loop::Continue => {}
            Loop::Break(v) => break v,
        }

        branch.ack().await;
    }
}

Full example: Rust Playground

I am by no means an expert in lifetimes, and I've sunken countless hours into this problem trying various tricks from other posts.

I believe the issue lies in that the lifetime of the future should match that of the closure, but adding such a bound forces the branch variable's lifetime to upgrade as well. This is problematic because branch should not have live for the entire repeat_new function body. This causes rust to throw an error stating that branch is dropped while still borrowed.

I am aware that using Box::pin can be used to circumvent this issue entirely but I chose not to use it for the following reasons:

  • Box::pin and async blocks pollute the code and reduce readability.
  • With pinned futures, you're often forced to clone all of the external data going into the closure for the sole reason of appeasing the compiler. Async closures don't have this problem because they handle the lifetimes of external data correctly.

This post seems to tackle a similar issue and I wonder if the solution comment (can't link because of the 2 link limit on new users, you'll have to scroll down) might be applicable here, but I have no idea how to go about doing that.

Thanks all

    pub async fn repeat_new<F, Fut, T>(&mut self, mut cb: F) -> T
    where
        for<'a> F: FnMut(&'a mut Ctx) -> Fut + 'a,
        Fut: Future<Output = Loop<T>> + Send,

One issue is that type parameters like Fut must resolve to a single type. And types that vary by lifetime are distinct types. That means Fut can't capture the 'a lifetime.[1] But future-returning closures do typically capture their generic inputs.

This bound in particular may not mean what you think it means:

for<'a> F: FnMut(&'a mut Ctx) -> Fut + 'a,
// same as
for<'a> F: 'a + FnMut(&'a mut Ctx) -> Fut,

The lifetime portion of the bound is putting a lifetime constraint on the closure, which probably isn't needed or desired. It's not doing anything to the Fut. It doesn't cause the Fut to be parameterized by the lifetime (which is probably what you wish it could do).


Another likely problem is that you probably need the future have a lifetime capped by it's other captures (e.g. external_data), and getting such upper bounds to apply to higher-ranked bounds (for<'a>) is hacky and limited.

AFAIK the goal still isn't obtainable on stable.[2] Attempted workarounds get stymied by various limitations in the language (very poor inference of borrow-capturing closures, inability to "funnel" the closures due unnameable future types, requirement to name return types in Fn bounds, inference breakdowns if you work around that requirement, lack of bounded for<'_>, GAT limitations...).

You can make it work by writing an actual function instead of using a closure, but only if you don't need to capture anything like external_data.


  1. i.e. can't have a type that involves 'a ↩︎

  2. Without unsafe anyway. ↩︎

1 Like

Thanks for the reply and links to related literature. I had low hopes of it being possible given how many hoops you have to jump through.

I'll be following the progress of async closure stabilization in the meantime.