Lifetimes of references for function taking async closure

Hello,

I'm trying to write a function taking a FnOnce async closure which is then executed in its body.
This closure should be called with a reference and should be able to use captured references, but I am not able to support both at the same time.

Sadly my knowledge of Rust lifetimes is not particularly great, but I think it should be possible to express this construct.
I would appreciate any help!

Here is my progress so far:

use ::core::future::Future;
use ::futures::future::{BoxFuture, FutureExt};

#[derive(Debug)]
struct Token();

async fn test<F, Fut>(f: F) // Attempt 1
where
    F: FnOnce(Token) -> Fut,
    Fut: Future<Output = ()>,
{
    let token = Token();
    f(token).await;
}

async fn test_ref<F>(f: F) // Attempt 2
where
    for<'a> F: FnOnce(&'a Token) -> BoxFuture<()> + 'a,
{
    let token = Token();
    f(&token).await;
}

async fn main() {
    let mut string = String::from("My string");

    // works, but argument to closure is not a reference
    let reference = &mut string;
    test(|token: Token| async move {
        reference.push('b');
        println!("{:?}", token);
    })
    .await;

    // works, but outer value is not taken by reference 
    test_ref(|token: &Token| async move {
        string.push('b');
        println!("{:?}", token);
    }.boxed())
    .await;
    
    // does not work
    let mut string = String::from("My string");
    let reference = &mut string;
    test_ref(|token: &Token| async move {
        reference.push('b');
        println!("{:?}", token);
    }.boxed())
    .await;
}

Error message:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `string` does not live long enough
  --> src/lib.rs:43:21
   |
43 |       let reference = &mut string;
   |                       ^^^^^^^^^^^ borrowed value does not live long enough
44 |       test_ref(|token: &Token| async move {
   |  ______________________________-
45 | |         reference.push('b');
46 | |         println!("{:?}", token);
47 | |     }.boxed())
   | |_____________- returning this value requires that `string` is borrowed for `'static`
48 |       .await;
49 |   }
   |   - `string` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

(Playground)

First of all, a small fix of your syntax :slightly_smiling_face: :

async fn test_ref<F>(f: F) // Attempt 2
where
-   for<'a> F: FnOnce(&'a Token) -> BoxFuture<()> + 'a,
+   for<'a> F: FnOnce(&'a Token) -> BoxFuture<'a, ()>,
{
    let token = Token();
    f(&token).await;
}

And from there, you get a "'static not satisfied" error, regarding which you may want to read:

Basically you have been correctly using for<'a> quantification to be able to handle the lifetime of the borrow to your local / interior token variable ("infinitely small lifetimes"), but sadly, for<…>, by itself, conveys to Rust that you also want the caller to be able to handle "infinitely big lifetimes" such as 'static.

One way to fix that is to also introduce the hack showcased in the aforementioned post:

- async fn test_ref<     F>(f: F) // Attempt 2
+ async fn test_ref<'up, F>(f: F) // Attempt 2
  where
-   for<'a> F: FnOnce(&'a Token) -> BoxFuture<'a, ()>,
+   for<'a> F: FnOnce(&'a Token, [&'a &'up (); 0]) -> BoxFuture<'a, ()>,
{
    let token = Token();
-   f(&token).await;
+   f(&token, []).await;
}

together with, at the call site:

-   test_ref(|token: &Token| async move {
+   test_ref(|token: &Token, []| async move {
        string.push('b');
        println!("{:?}", token);
    }.boxed())
    .await;

Playground


Note that if you control the definition of Token, you could smuggle the 'upper bound lifetime in there, and thus avoid exposing that [] hack to the callers:

#[derive(Debug)]
struct Token<'up>(::core::marker::PhantomData<&'up ()>);

async fn test_ref<'up, F>(f: F) // Attempt 2
where
    for<'a> F: FnOnce(&'a Token<'up>) -> BoxFuture<'a, ()>,
{
    let token = Token(<_>::default());
    f(&token).await;
}
4 Likes

Thank you very much for your extensive answer!

Would you know if there are any ideas to improve the ergonomics in such cases, like the possibility to specify explicit bounds for higher-order lifetimes?

Do you know why that -> BoxFuture<()> + 'a isn't a hard error? I figure the + 'a is understood as an additional bound on F (effectively requiring F: 'static), but why is it allowed to omit the lifetime parameter of BoxFuture?

Edit: Answering my own question… if you deny(elided_lifetimes_in_paths) you get an error, so I guess BoxFuture<()> is understood as BoxFuture<'_, ()> here :‌|

1 Like

Yeah, it's an incredibly unlucky combination of circumstances, I'd say. Tangentially, I'd personally like an opt-in lint that would warn against + … bounds after Fn… bounds, since I find those confusing or somewhat error-prone, as this unlucky example has shown :sweat_smile: (I already personally write stuff as F : 'a + Send + Fn…. That is, "Fn… goes last").

I don't know of those, but I do know these "implicit bounds through types involved", whether hackily ([; 0] array), or just cumbersomely (<'up> parameter on Token), is coming up more frequently as people adventure into more lifetime-complex signatures.

  • Another example: the stdlib's scoped-threads API currently involves a
    &'scope Scope<'scope, 'env>
    
    parameter, when with for<'scope where 'env : 'scope> quantifications we could just have had:
    &'scope Scope<'scope>
    
    similarly, to your Token<'up> situation.

So the lack of for<… where …> is being quite annoying in that regard. So yeah, I'd love to see for<… where …> being developed :pray:

2 Likes