Lifetime GATs for oldschool async closures

Hi, I'm having a problem mixing HRTBs with (old-style) async closures.

Context:

The second resource describes a workaround for the issue described in the first resource. It allows one to specify HRTBs of the form for<'a where 'a: 'limit> through an auxiliary trait. This works for structs, but I am trying to apply this same idea to closures that return futures (playground):

use futures::future::BoxFuture;

/// Trait to enforce that 'limit : 'bounded
trait LtBound<'limit, 'bounded, _LifetimeConstraint = &'bounded &'limit ()>:
    FnOnce(&'bounded Arg) -> BoxFuture<'bounded, ()>
{}
impl<'limit, 'bounded, Impl> LtBound<'limit, 'bounded> for Impl where
    Impl: FnOnce(&'bounded Arg) -> BoxFuture<'bounded, ()>
{}

/// Trait to close over 'bounded
trait DelimitedFnOnce<'limit>: for<'bounded> LtBound<'limit, 'bounded> {}

impl<'limit, Impl> DelimitedFnOnce<'limit> for Impl where
    for<'bounded> Impl: LtBound<'limit, 'bounded>
{
}


struct Arg;
struct Captured;

/// Function that requires the bound.
fn update_arg<'a, F>(_captured: &'a Captured, _update_fun: F)
where
    F: DelimitedFnOnce<'a>,
{}

// Test: closure that closes over `&'a Captured` and passes 
// it to `update_arg`. This is conceptually fine given the bound
// `F: DelimitedFnOnce<'a>` (something something coercion sites)
fn test<'a>(s: &'a Captured) {
    update_arg::<'a>(s, move |_| {
        Box::pin(async move {
            let _s = s;
        })
    });
}

This fails with the error I was trying to avoid:

error: lifetime may not live long enough
  --> src/lib.rs:36:9
   |
34 |   fn test<'a>(s: &'a Captured) {
   |           -- lifetime `'a` defined here
35 |       s.update_arg::<'a>(move |_| {
36 | /         Box::pin(async move {
37 | |             let _s = s;
38 | |         })
   | |__________^ returning this value requires that `'a` must outlive `'static`

error: could not compile `playground` (lib) due to 1 previous error

I conceptually expect this to work because DelimitedFnOnce<'a> on update_arg basically says that our FnOnce can only be invoked with lifetimes for<'b where 'a: 'b>.
I assume the reason this doesn't work is either that type inference first derives a higher-rank type for the closure using a unification variable, and fails before even considering F: DelimitedFnOnce<'a> on the function call, or that somehow the implicit bound is lost along the way.

I think two issues are that

  • You'll probably need to drive closure inference by using a : Fn* bound, which are special (a subtrait of Fn* doesn't drive closure inference for example)
  • Sometimes HRTBs impose 'static in ways the alternative doesn't work around

The first point being something I've concretely wrestled with, and the second point being more of a feeling.

You can deal with the first bullet point to some extent by sneaking the implicit lifetime bound into the closure bound.

fn drive_inference<'limit, F>(f: F) -> F
where
    for<'bounded> F: FnOnce(
        &'bounded Arg,
        [&'bounded &'limit (); 0] // <-- for<'b where 'a: 'b> emulator
    ) -> BoxFuture<'bounded, ()>
{
    f
}

This gets you closer to your goal (I think):

Oh, I never realized the Fn*-traits had this interaction with wrapper traits during type inference; that's a good pointer, thanks! :slight_smile:

I dont actually need the DelimitedFnOnce trait; I just introduced it to have a place to put the 'limit : 'bound constraint, so the whole thing simplifies to: Rust Playground

It's a shame the extra argument is necessary, but on the other hand, no extra traits are required, so that's great.

I had hoped to build upon this by moving the argument type into the generic associated type of a trait that has the lifetime bound (so we don't need an extra arg to specify it), but unfortunately it seems like implicit bounds on associated types are not propagated: Rust Playground

You can get a little further (the implementations succeed but test fails) by adding explicit bounds to the GAT, which looks like the "you put a bound on a lifetime so HRTB fails" situation to me, and I think at this point we're basically circling back round to Sabrina's blog post.

Or this issue or another one. Anyway, yeah, I haven't found a way around this issue either.