Lifetime issues with input capturing future producing fn

I am trying to figure out how to write an api that takes a context (&mut C) and a future producing closure/fn. The closure should be called with a mutable reference to the context (&mut &mut C) and produces a future containing that mutable reference to the context.

Some of my first attempts looked like this:

use core::future::Future;

struct AuthSession;

impl AuthSession {
    fn app_id(&mut self) -> i32 {
        1
    }
}

trait InputCapturingFn<'a, I: 'a, O: 'a>: FnMut(&'a mut I) -> O {}

impl<'a, I: 'a, O: 'a, F: FnMut(&'a mut I) -> O> InputCapturingFn<'a, I, O> for F {}

fn retry_with_context<C, Fn, Fut>(
    context: C,
    mut operation: Fn,
) -> impl Future<Output = Result<(), ()>>
where
    Fn: for<'c> InputCapturingFn<'c, C, Fut>,
    Fut: Future<Output = ()>,
    C: Send,
{
    async move {
        let mut context = context;
        let _res = operation(&mut context).await;
        let res = operation(&mut context).await;
        Ok(res)
    }
}

async fn test(auth_session: &mut AuthSession) {
    retry_with_context(
        auth_session,
        |auth_session: &mut &mut AuthSession| async move {
            let auth_session = auth_session;
            let _ = auth_session.app_id();
        },
    )
    .await;
}

The compiler errors with:

error: lifetime may not live long enough
  --> src/lib.rs:40:47
   |
40 |           |auth_session: &mut &mut AuthSession| async move {
   |  ________________________-____________________-_^
   | |                        |                    |
   | |                        |                    return type of closure `impl Future<Output = ()>` contains a lifetime `'2`
   | |                        let's call the lifetime of this reference `'1`
41 | |             let auth_session = auth_session;
42 | |             let _ = auth_session.app_id();
43 | |         },
   | |_________^ returning this value requires that `'1` must outlive `'2`

error: lifetime may not live long enough
  --> src/lib.rs:40:47
   |
40 |           |auth_session: &mut &mut AuthSession| async move {
   |  _____________________________-_______________-_^
   | |                             |               |
   | |                             |               return type of closure `impl Future<Output = ()>` contains a lifetime `'4`
   | |                             let's call the lifetime of this reference `'3`
41 | |             let auth_session = auth_session;
42 | |             let _ = auth_session.app_id();
43 | |         },
   | |_________^ returning this value requires that `'3` must outlive `'4`
   |
   = note: requirement occurs because of a mutable reference to `&mut AuthSession`
   = note: mutable references are invariant over their type parameter
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

error[E0521]: borrowed data escapes outside of function
  --> src/lib.rs:38:5
   |
37 |   async fn test(auth_session: &mut AuthSession) {
   |                 ------------  - let's call the lifetime of this reference `'1`
   |                 |
   |                 `auth_session` is a reference that is only valid in the function body
38 | /     retry_with_context(
39 | |         auth_session,
40 | |         |auth_session: &mut &mut AuthSession| async move {
41 | |             let auth_session = auth_session;
42 | |             let _ = auth_session.app_id();
43 | |         },
44 | |     )
   | |     ^
   | |     |
   | |_____`auth_session` escapes the function body here
   |       argument requires that `'1` must outlive `'static`

I was hoping InputCapturingFn<'a, I: 'a, O: 'a>: FnMut(&'a mut I) -> O could've provided enough information that the output captures the input but regardless the compiler produces: returning this value requires that `'1` must outlive `'2`.

What am I missing? Is there any way to express this pattern?

Playground link

There's currently no way to get |param: &Value| async move { ... } closures working. The compiler simply can't recognize that the closure is supposed to be generic over lifetimes when working through helper traits, which are necessary to express the correct bounds at all. Perhaps it will be possible in the future, either through better closure type inference or explicit annotations (such as for<'a> lifetime binders, although those currently are insufficient for this).

The current options are to use an async fn instead of a closure, or to make the closure return a Pin<Box<dyn Future<Output = T> + Send + 'lt>> for some suitable 'lt. The futures crate has a BoxFuture<'lt, T> type for this purpose, and an extension method .boxed() that can be called on a future in order to convert it to a BoxFuture.

Even without closure, it doesn't really work:

use core::future::Future;

struct AuthSession;

impl AuthSession {
    fn app_id(&mut self) -> i32 {
        1
    }
}

trait Captures<'a> {}

impl<'a, T> Captures<'a> for T {}

fn retry_with_context<C, Fn, Fut>(
    context: C,
    mut operation: Fn,
) -> impl Future<Output = Result<(), ()>>
where
    Fn: for<'a> FnMut(&'a mut C) -> Fut,
    Fut: Future<Output = ()>,
    C: Send,
{
    async move {
        let mut context = context;
        let _res = operation(&mut context).await;
        let res = operation(&mut context).await;
        Ok(res)
    }
}

fn dodo<'a, 'b: 'a>(auth_session: &'a mut &'b mut AuthSession) -> impl Future<Output = ()> + 'a + Captures<'b> {
    async move {
        let auth_session = auth_session;
        let _ = auth_session.app_id();
    }
}

async fn test<'a>(auth_session: &'a mut AuthSession) {
    retry_with_context(
        auth_session,
        dodo,
    )
    .await;
}

Playground

The last error:

error[E0521]: borrowed data escapes outside of function
  --> src/lib.rs:42:5
   |
41 |   async fn test<'a>(auth_session: &'a mut AuthSession) {
   |                 --  ------------ `auth_session` is a reference that is only valid in the function body
   |                 |
   |                 lifetime `'a` defined here
42 | /     retry_with_context(
43 | |         auth_session,
44 | |         dodo,
45 | |     )
   | |     ^
   | |     |
   | |_____`auth_session` escapes the function body here
   |       argument requires that `'a` must outlive `'static`

might be because the hrtb Fn: for<'a> FnMut(&'a mut C) -> Fut implies that 'a must be 'static. However, what we really want is: for<'a> if (C: 'a) FnMut(&'a mut C).

There, the problem is that you're constraining the future to a single type Fut, even though it really has to be generic over 'a. You still have to include a helper trait to get it to work (Rust Playground). Also, if you use a real async fn() instead of a fn() -> impl Future (Rust Playground), then you can ditch the Captures<'b> hack.

1 Like

Ahh, I've actually tried this before (errored implementation of `FnOnce` is not general enough) but there was one difference in the code:

My dodo had an extra outlives bound:

fn dodo<'a, 'b: 'a>(auth_session: &'a mut &'b mut AuthSession) -> impl Future<Output = ()> + 'a + Captures<'b> {
   ...
}

I'd assumed both were the same because of the implied bound ('b: 'a)....

Yeah, writing the bound explicitly like that prevents the compiler from generalizing the function type into a for<'a, 'b> fn(...) (it makes the lifetime early-bound instead of late-bound). The solution is to only ever include it as an implied bound in the parameter types; I've seen some people add dummy parameters just to add the implied bounds.

In general, I'd try to avoid working with generic functions and closures in this way, simply due to the number of hacks you have to stay on top of in order to get them working right. Instead, I'd use something like async-trait to implement the generic functionality.

2 Likes

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.