HRTB lifetimes and traits for async functions

Hi everyone,

I'm refering to this topic here. I tinkered around a little bit. :slight_smile: This example should illustrate a standard case where async_fn_traits can help.

My understanding know is that you cannot tie an opaque return value like a Future to a HRTB lifetime with the Fn(T) -> U syntax therefore you need something like the async_fn_traits workaround (as already has been mentioned in the other topic).

But there's still one puzzle piece that I'm missing: When I use for example AsyncFn1 like in the outcommented part of the playground snippet above where exactly in the trait definition of AsyncFn1 is the part that ensures that the lifetime of the async function output (impl Future<Output=()> + '_) is the same as the lifetime of the exclusive reference which gets passed to test_func in the example? ... Might it be that passing &'a mut u32 as generic parameter for AsyncFn1 implies this?


Let's give these names:

// TheFn                     TheFuture<'a>
// vvvvvvvvv                 vvvvvvvvvvvvvvvvvvvvvvvvvvv
fn test_func(x: &mut u32) -> impl Future<Output=()> + '_ {
    async {
        *x = 2;

// Compiler supplied implementations:
impl<'a> FnOnce<(&'a mut u32,)> for TheFn {
    type Output = TheFuture<'a>;
    /* ... */ 
impl<'a> FnMut<(&'a mut u32,)> for TheFn { /* ... */ }
impl<'a> Fn<(&'a mut u32,)> for TheFn { /* ... */ }
impl<'a> Future for TheFuture<'a> { type Output = (); /* ... */ }

Then as part of the blanket implementation:

impl<F: ?Sized, Fut, Arg0> AsyncFn1<Arg0> for F where
    F: Fn(Arg0) -> Fut,
    Fut: Future, 

The types above are handled as-if there was an more specific implementation:

impl<'a> AsyncFn1<&'a mut u32> for TheFn {
    type OutputFuture = TheFuture<'a>;
    type Output = ();

Because that's what the signature of test_fn matches.

The trait definition doesn't force a relationship between the input and the output, but it allows one. If the output has a non-'static lifetime, it must have come from the input (Arg0) -- there's nowhere else for it to come from in the implementation.

But the output could also be always 'static, or capture some but not all non-'static lifetimes from the input, etc.

The Fn traits similarly allow but do not force a relationship between input and output (else we couldn't have non-borrowing functions). The key differences from the Fn traits are

  • You're not forced to name the associated output type in bounds
    • E.g. the R in your playground, which must be a single type, not a type constructor with a lifetime parameter
  • Having the bound you desire (Future<...>) on the associated type
  • Maybe just straight-up more flexible with HRTB not forcing 'static too, though that's not important for this example

Thanks so much! Now the matter is clear to me. :slight_smile:

Sorry, for bringing the topic back to the top, just one last question:

The formal reason that the code snippet doesn't compile is that fn types are contravariant covariant over their input output parameter, right?

It's because the return value needs to be a different type depending on the input lifetime/type.

Types that differ only by lifetime are still distinct types, and something like &str (for any lifetime) isn't a type - it's a type constructor for every concrete &'a str, &'b str...

The variance doesn't matter; they're distinct types whether covariant, contravariant, or invariant.

The conflict is that R must be a single type (and thus independent of the input lifetime/type). (And Rust doesn't have generic type constructor parameters, so there's no direct fix.)

Make sense?

1 Like

Yes! :slight_smile: Thanks again. :wink:

Due to the compiler message:

   = note: expected opaque type `impl for<'a> Future<Output = ()> + '_`
              found opaque type `impl Future<Output = ()> + '_`

I thought you could argue with covariance as the return type of test_func ist more general than the return type of F in test_wrapper but as you explained this seems to have nothing to do with variance.

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.