Control lifetime for parameter of a generic async function

I have such a scene:

Call a generic async function, then use its result to call another function, all using a same parameter &mut context.

Here is an example:

use futures::Future;

fn tester<F, Fut>(step1: F) where F: Fn(&mut u8) -> Fut { // Here Fut should be a future, don't matter
    let mut i: u8 = 0;
    step1(&mut i); 
    step2(&mut i);
    // In real code it's:
    // step1(reference).await.step2(reference);
} 

// A and B are actually same.
async fn step1_A(_: &mut u8) {}
fn step1_B<'a>(_: &'a mut u8) -> impl Future<Output=()> + 'a {
    async move {}
}

fn step2(_: &mut u8) {}

fn main() {
    let step1_C = |_: &mut u8| async move { };
    tester(step1_A);
    tester(step1_B);
    tester(step1_C);
}

Here step1_A and step1_B don't compile, because

mismatched types
expected associated type `<for<'_> fn(&mut u8) -> impl for<'_> futures::Future {step1_A} as FnOnce<(&mut u8,)>>::Output`
   found associated type `<for<'_> fn(&mut u8) -> impl for<'_> futures::Future {step1_A} as FnOnce<(&mut u8,)>>::Output`
the required lifetime does not necessarily outlive the empty lifetime

Compiler can't infer correct lifetime, so I should point It out:

// Using HRTB don't work, error is the same
fn tester<F, Fut>(step1: F) where for <'a> F: Fn(&'a mut u8) -> Fut + 'a

Setting lifetime as function generic parameter works:

fn tester<'a, F, Fut>(step1: F) where F: Fn(&'a mut u8) -> Fut

But now calling step2 don't compile, because 'a is a uncontrolled lifetime, step1 may keep the i and don't return, and even live longer than i lives.

I think the lifetime is quite clean here: the context (i: u8 here) should be rent by step1,get a future, consume that future, return a value, meantime return the controller of the context, and rent context to step2.

Writing a closure can make them all compile, but I want to know if there is a way to make fn case works?

The problem is that here:

fn tester<F, Fut>(step1: F) where F: Fn(&mut u8) -> Fut

The type parameter Fut represents a single type, so F must return the same type for every input lifetime. This isn't sufficient as the result of step1_A and step1_B capture the input lifetime, and thus return a different type for each input lifetime. (Types that differ only in lifetime are still distinct types.)

You can work around this with some GAT-like helper traits to avoid having to mention the return types by name (thus avoiding the type parameter which is currently restricting you to a single returned type).

Playground. The T: 'a bounds may not be required, and more tweaking may be necessary based on your actual use case.

3 Likes