Search code examples
rustasync-awaitclosures

Async closure holding reference over await point


Is there a workaround for creating an async closure that holds a reference over an await point?

Example:

use std::time::Duration;
use tokio::time::sleep;

fn main() {
    let closure = |v: &u64| async move {
        sleep(Duration::from_secs(1)).await;
        println!("{}", v);
    };
}

playground

Fails with:

error: lifetime may not live long enough
 --> src/main.rs:5:29
  |
5 |       let closure = |v: &u64| async move {
  |  _______________________-___-_^
  | |                       |   |
  | |                       |   return type of closure `[async block@src/main.rs:5:29: 8:6]` contains a lifetime `'2`
  | |                       let's call the lifetime of this reference `'1`
6 | |         sleep(Duration::from_secs(1)).await;
7 | |         println!("{}", v);
8 | |     };
  | |_____^ returning this value requires that `'1` must outlive `'2`

I'm aware that async closures could help. This works:

#![feature(async_closure)]

use std::time::Duration;
use tokio::time::sleep;

fn main() {
    let closure = async move |v: &u64| {
        sleep(Duration::from_secs(1)).await;
        println!("{}", v);
    };
}

playground

but given that they're not stable yet, I was wondering if there is any other workaround to make this work.


Solution

  • The problem is that the compiler infers the closure to accept &'some_lifetime u64 instead of &'any_lifetime u64 as it would do for functions.

    Usually, when this is a problem we can pass the closure into a function that takes it and returns it but constrain the closure to be for<'a> Fn(&'a u64) (or just Fn(&u64)), and this helps the compiler infer the right lifetime. But here we cannot do that, because such function will disallow the returned future to borrow from the parameter, as I explained in Calling a generic async function with a (mutably) borrowed argument.

    If you can change the closure to a function, this is the simplest solution.

    Otherwise, if the closure captures, you can box the returned future and then use the aforementioned function:

    use std::future::Future;
    use std::pin::Pin;
    
    fn force_hrtb<F: Fn(&u64) -> Pin<Box<dyn Future<Output = ()> + '_>>>(f: F) -> F {
        f
    }
    
    let closure = force_hrtb(|v: &u64| {
        Box::pin(async move {
            sleep(Duration::from_secs(1)).await;
            println!("{}", v);
        })
    });
    

    If the cost of boxing and dynamic dispatch is unacceptable, but you can use nightly, you can use the unstable feature closure_lifetime_binder to force the compiler to treate &u64 as &'any_lifetime u64. Unfortunately, because closure_lifetime_binder requires the return type to be written explicitly, we also need to do that and we can do that only with another unstable feature, type_alias_impl_trait:

    #![feature(closure_lifetime_binder, type_alias_impl_trait)]
        
    use std::future::Future;
    
    type ClosureRet<'a> = impl Future<Output = ()> + 'a;
    let closure = for<'a> |v: &'a u64| -> ClosureRet<'a> {
        async move {
            sleep(Duration::from_secs(1)).await;
            println!("{}", v);
        }
    };