Async Closure that References Arguments

I'm trying to write an async function that references an argument. This works fine for a function. Extremely simplified example below.

pub async fn async_clone(s: &str) -> String {
    s.to_string()
}

Rust Playground

But for my purpose I need to use a closure for ergonomics and it doesn't seem to work.

let async_clone = |s: &str| async move {
    s.to_string()
};

The error:

error: lifetime may not live long enough
 --> src/lib.rs:2:33
  |
2 |       let async_clone = |s: &str| async move {
  |  ___________________________-___-_^
  | |                           |   |
  | |                           |   return type of closure `impl Future<Output = [async output]>` contains a lifetime `'2`
  | |                           let's call the lifetime of this reference `'1`
3 | |         s.to_string()
4 | |     };
  | |_____^ returning this value requires that `'1` must outlive `'2`

error: could not compile `playground` due to previous error

Rust Playground

The error for "proper" async closures on nightly is identical

It seems that the problem is that the compiler assumes that the output type should be 'static for the closure example but the default inference rules for function allow for <'a> Fn(&'a str) -> impl std::future::Future<..> + 'a for the function. Is there a way to allow this less strict requirement for closures. (And does anyone know if there is a good reason for it to behave like this?)

RFC #3216 might help with this issue by allowing explicit lifetime bindings on the closure. (Or maybe not, since impl Future isn't allowed as an explicit return type.) This comment notes this exact scenario. It would be better if this worked implicitly, though.

An async fn does not run to_string() when you call it! It makes a Future struct that keeps the s reference inside until it's polled for the first time. Your async block really does hold on to &str for its arbitrary lifetime. So it's not an issue of declaring that lifetime of the input argument is unrelated to output, because they are actually related.

The solution to this is to convert to String outside of an async context. It's not possible in async fn, because everything in it is inside the async context, including its arguments. You'd have to wrap it in a regular fn that returns impl Future.

In case of a closure you can:

let async_clone = |s: &str| { let s = s.to_string(); async move { s } };
2 Likes

In my case I need to use the reference in the async block. I know that this workaround can be used in the simplified example but it doesn't help the general case where you need to hold the reference.

Thanks for the direction!

In that RFC thread I saw a hint that made it work. It seems that the problem is that the type inference doesn't have enough to go on and assumes 'static. If you help it out it actually works as expected.

Helping it out is a bit awkward because you can't give constraint hints in a let binding but you can define a function to give the hint.

fn hint_lifetime<
    'a,
    Fut: std::future::Future<Output=String> + 'a,
    Fun: FnOnce(&'a str) -> Fut,
>(f: Fun) -> Fun {
    f
}
    
let async_clone = hint_lifetime(|s: &str| async move {
    s.to_string()
});

Playground

1 Like

Careful, this may not be doing what you expect, once you go back to an actual use-case: this is making the closure itself not be higher-order, so it won't be able to accept borrows to locals provided by the callee.

That is, your resulting async_clone closure won't be usable within the following API:

use ::async_fn_traits::AsyncFn1;

async
fn with_local_string<R, AsyncCb> (async_callback: AsyncCb) -> R
where
    AsyncCb : for<'any> AsyncFn1<&'any str, Output = R>,
{
    let local = String::from("…");
    cb(& /* pick 'any = 'local */ local).await
}
  • where I'm using the convenience traits to express proper higher-order async callback bounds that are featured by the ::async_fn_traits crate.

So basically your helper function is just making the input of the closure not be higher-order, which you could also achieve by moving the type annotation for s from the closure's parameter list to its body:

- |s: &str| async move {
+ |s| async move {
+     let _: &str = s;
      …
  }

(feel free to try that).


A proper solution would be to use something like:

and define your closure as:

let async_clone = hrtb!(|s: &'_ str| -> BoxFuture<'_, String> {
    async move {
        …
    }.boxed()
});
  • (which will be using something analogous to your hint_lifetime helper, but for explicitly using BoxFuture<'_, String>, a future generic over that higher-order '_ lifetime parameter)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.