async fn unsugaring / lifetime

Consider, for instance:

trait Trait {
    async fn async_fn<'a, 'b>(
        a: Arc<Mutex<&'a str>>,
        b: Arc<Mutex<&'b str>>,
    ) -> i32
    {
        dbg!(a.lock());
        dbg!(b.lock());
        42
    }
}

As of 1.66.1, in Rust, this cannot be written directly, since

async fn f(…) -> Ret

is sugar for

fn f(…) -> impl Future<Output = Ret>

and -> impl Trait cannot be used in traits yet.

In the case of Future, which is a dyn-able trait, and which is Pin<Box>-transitive (i.e., given T : ?Sized, if T : Future then Pin<Box<T>> : Future<Output = T::Output>), a workaround for this limitation is thus to write the function as -> Pin<Box<dyn Future…>>.

And since the process is:

  • tedious,
  • quite automatable,
  • and yet a bit subtle at times,

we end up with —you guessed it— macros to do this for us, such as #[async_trait]

But how does #[async_trait] do it?

  • Answering this question is more important than just intellectual curiosity. Indeed, what if:

To anticipate any of these cases, it's actually very sensible to learn to do what #[async_trait] does.

  1. So we start off

    trait Trait {
        async fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> i32
        {
            …
        }
    }
    
  2. We replace async fn … -> Ret with fn … -> impl Future<Output = Ret>:

    trait Trait {
        fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> impl Future<Output = i32>
                + InfectedBy<'a> + InfectedBy<'b>
        {
            async move {
                let _captures = (&a, &b);
                …
            }
        }
    }
    
  3. We Pin<Box>-wrap it:

    trait Trait {
        fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> Pin<Box<
                impl Future<Output = i32>
                   + InfectedBy<'a> + InfectedBy<'b>
            >>
        {
            Box::pin(async move {
                let _captures = (&a, &b);
                …
            })
        }
    }
    
  4. We let it dyn:

    ) -> Pin<Box<
    -       impl Future<Output = i32>
    +        dyn Future<Output = i32>
               + InfectedBy<'a> + InfectedBy<'b>
        >>
        {
            Box::pin(async move {
                let _captures = (&a, &b);
                …
            })
        }
    }
    
    • We won't bother with Send-ness, here.
  5. We can't really use + InfectedBy<'_> with dyn (and don't really need to, as we'll see below), so we get rid of that too:

          dyn Future<Output = i32>
    -       + InfectedBy<'a> + InfectedBy<'b>
    +       + '???
    

At this point we've ended up with the following return type:

dyn '??? + Future<Output = i32>

And now the million dogecoin question is to know which lifetime we put here:

  • dyn 'a + Future… ?
    • May cover a region outside that of 'b, so our future may dangle in there: ❌
  • dyn 'b + Future… ?
    • symmetrical situation: ❌
  • dyn 'a + 'b + Future… ?
    • Papering over the fact Rust doesn't let us write dyn 'a + 'b + … for some reason, this cannot be right, since this expresses a 'usability that includes 'a and 'b, and we've seen that each of these is already problematic, so a bigger usability will be just as problematic, if not more!

I've talked about all this in more detail over the section about -> impl Trait, which also happens to mention the answer.

The gist of the answer is that:

An impl InfectedBy<'a> + InfectedBy<'b> is only usable within the intersection of 'a and 'b.

InfectedBy<'a, 'b> : 'a ^ 'b

In other words, our '??? / the "'usability" of such an entity is 'a ^ 'b.

So we end up needing to write:

//! Pseudo-code

fn async_fn<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>

This, as the non-existing-in-Rust 'a ^ 'b syntax suggests, is not real Rust code. See the intersection lifetime section to know more about how the above ends up becoming:

//! Real-code!

fn async_fn<'fut, 'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'fut, i32>
where
    // ⊇
    'a : 'fut, 'b : 'fut,
 // 'a ^ 'b ⊇ 'fut