1

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.

Heinzi
  • 5,793
  • 4
  • 40
  • 69

2 Answers2

1

The issue here is that there's no way to indicate that the closure's return includes the reference. Normally you can do this with impl syntax, but that's not allowed on closures.

// not allowed
|v: &'a u64| -> impl Future + 'a

So instead, you can make it an async function, which does this automatically.

fn main() {
    let closure = |v| c(v);
}

async fn c(v: &u64) {
    sleep(Duration::from_secs(1)).await;
    println!("{}", v);
}

If you need to capture anything, you can add it to the function arguments.

drewtato
  • 6,783
  • 1
  • 12
  • 17
1

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);
    }
};
Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77