Why does the closure expression that returns a future have a lifetime issue?

Consider this example:

async fn test(v:&i32)->&i32{  // #1
    v
}

fn main(){
    let f = |v:&i32| async move{  // #2
        v
    };
}

#2 can result in a compile error that says:

  |
7 |       let f = |v:&i32| async move{
  |  ________________-___-_^
  | |                |   |
  | |                |   return type of closure `{async block@src/main.rs:7:22: 9:6}` contains a lifetime `'2`
  | |                let's call the lifetime of this reference `'1`
8 | |         v
9 | |     };
  | |_____^ returning this value requires that `'1` must outlive `'2`

However, Shouldn't #2 be similar to #1? It appears to me that #2 can be de-sugared to

for<'a> |v:&'a i32| -> impl Future<Output = &'a i32> {
      async move{
          v
    }
}

which is exactly what #1 does. As described in 3498-lifetime-capture-rules-2024 - The Rust RFC Book, the returned Future captures the lifetime and implicitly requires that 'a: ReturnedFuture.

So, why is #1 ok but #2 is an error?

The compiler poorly infers it as something like

// It doesn't infer it as generic/HR really, it infers
// it as returning one specific lifetime, but anyway
async fn test<'a>(v:&i32) -> &'a i32 {  // #1
    v
}

Which is the same problem as the snippet below. I.e. the compiler is extremely bad at higher-ranked closure inference.

// error: lifetime may not live long enough
let lambda = |v: &i32| v;

You can correct it in the typicaly way... except you also need some type erasure due to not being able to name future types.

fn funnel<F>(f: F) -> F
where
    F: Fn(&i32) -> Pin<Box<dyn Future<Output = &i32> + '_>>
{
    f
}

fn main(){
    let f = funnel(|v: &i32| Box::pin(async move {
        v
    }));
}

'a: Type isn't something that has meaning in Rust, and ReturnedFuture: 'a isn't the result of capturing either.

But yes, async fn return types capture all generics.

I wonder whether this workaround does the same things as the following

fn funnel<F>(f: F) -> F
where
    F: for<'a> Fn(&'a i32) -> Pin<Box<dyn Future<Output = &'a i32> + 'a>>
{
    f
}

This is such that the input lifetime is associated with the output. Moreover, if we had a closure binder, then the compile error could be resolved as simple as:

#![feature(closure_lifetime_binder)]
fn main(){
   let lambda = for<'a> |v: &'a i32|->&'a i32 { v };
}

The crucial issue here is that the compiler cannot associate the input lifetime with the output for the closure expression, right?

Yes, those bounds mean the same thing.

For the non-async version or the Pin<Box<dyn>> versions, yes. For the unboxed future, we'd need -> impl Trait in closure declarations or such in addition (as the futures are not nameable).

Yes, or at least it does not without something like the funnel workaround -- in combination with the fact that lifetime elision in closure declarations doesn't work the same as lifetime elision in function signatures, Fn bounds, etc.

Many cases work when you have enough control to steer the compiler, so it is capable in more ways than can be easily expressed. But the language has yet to give programmers enough control in a complete or ergonomic manner.

(And probably it will become more capable with time, too.)

1 Like

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.