async_trait's one weird type ascription trick

Now that I got the Log in with GitHub feature working, let's explore what this would've looked like with the async_trait crate.

First up, the trait definition:

Rust code
/// Something that can refresh credentials
#[async_trait::async_trait]
pub trait CredentialsRefresher {
    async fn refresh(&self, creds: &FutileCredentials) -> eyre::Result<FutileCredentials>;
}

Let's look at its expanded form, because I'm curious (using the rust-analyzer command "Expand macro recursively"):

Rust code
#[doc = " Something that can refresh credentials"]
pub trait CredentialsRefresher {
    #[must_use]
    #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
    fn refresh<'life0, 'life1, 'async_trait>(
        &'life0 self,
        creds: &'life1 FutileCredentials,
    ) -> ::core::pin::Pin<
        Box<
            dyn ::core::future::Future<Output = eyre::Result<FutileCredentials>>
                + ::core::marker::Send
                + 'async_trait,
        >,
    >
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait;
}

Interesting! Pretty similar, honestly, except more lifetimes.

Then the implementation:

Rust code
#[async_trait::async_trait]
impl CredentialsRefresher for ServerState {
    async fn refresh(&self, creds: &FutileCredentials) -> eyre::Result<FutileCredentials> {
        let patreon_id = // etc.

        // same code, except not wrapped in a `Box::pin(async move {})`

        Ok(fut_creds)
    }
}

If we expand the implementation, we find...

Rust code
impl CredentialsRefresher for ServerState {
    #[allow(
        clippy::let_unit_value,
        clippy::no_effect_underscore_binding,
        clippy::shadow_same,
        clippy::type_complexity,
        clippy::type_repetition_in_bounds,
        clippy::used_underscore_binding
    )]
    fn refresh<'life0, 'life1, 'async_trait>(
        &'life0 self,
        creds: &'life1 FutileCredentials,
    ) -> ::core::pin::Pin<
        Box<
            dyn ::core::future::Future<Output = eyre::Result<FutileCredentials>>
                + ::core::marker::Send
                + 'async_trait,
        >,
    >
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            if let ::core::option::Option::Some(__ret) =
                ::core::option::Option::None::<eyre::Result<FutileCredentials>>
            {
                return __ret;
            }
            let __self = self;
            let creds = creds;
            let __ret: eyre::Result<FutileCredentials> = {
                let patreon_id = creds
                    .user_info
                    .profile
                    .patreon_id
                    .as_deref()
                    .ok_or_else(|| eyre::eyre!("can only renew patreon credentials"))?;
                
                // same code as before, except weird because _all macros_ get
                // expanded

                Ok(fut_creds)
            };
            #[allow(unreachable_code)]
            __ret
        })
    }
}

Same tricks, plus one more: look at this closer:

Rust code
if let ::core::option::Option::Some(__ret) =
    ::core::option::Option::None::<eyre::Result<FutileCredentials>>
{
    return __ret;
}

That's... suspicious.

It very much is! Do you know what it's for?

No, but I'm assuming you'll tell us?

I will! Let's start with a closure: what's the return type of this one?

Rust code
fn main() {
    let closure = || {
        let x: u64 = 1337;
        Ok(x)
    };
}

It's... Result of... something?

Correct! It's impossible to infer:

Shell session
$ cargo check --quiet
error[E0282]: type annotations needed
 --> src/main.rs:4:9
  |
4 |         Ok(x)
  |         ^^ cannot infer type of the type parameter `E` declared on the enum `Result`
  |
help: consider specifying the generic arguments
  |
4 |         Ok::<u64, E>(x)
  |           ++++++++++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `ascription` due to previous error

We can resolve it like the compiler said we should:

Rust code
    let closure = || {
        let x: u64 = 1337;
        Ok::<u64, std::convert::Infallible>(x)
    };

Or we can specify the return type of the closure:

Rust code
    let closure = || -> Result<_, std::convert::Infallible> {
        let x: u64 = 1337;
        Ok(x)
    };

(We can even let the compiler infer the first generic parameter of Result! How neat!)

Now with async blocks! What's the return type of:

Rust code
fn main() {
    let async_block = async {
        let x: u64 = 1337;
        Ok(x)
    };
}

I'm having déjà-vu.

Correct!

Shell session
$ cargo check --quiet
error[E0282]: type annotations needed
 --> src/main.rs:4:9
  |
4 |         Ok(x)
  |         ^^ cannot infer type of the type parameter `E` declared on the enum `Result`
  |
help: consider specifying the generic arguments
  |
4 |         Ok::<u64, E>(x)
  |           ++++++++++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `ascription` due to previous error

Here too, we can resolve the ambiguity by slapping a turbofish on the Ok, but... async-trait is a procedural macro crate. Can you see how it would be annoying to transform just the right Ok / Err occurrences? It's just not very machine-friendly.

And there's no syntax to "annotate the return type of an async block" either. Technically, it doesn't even return!

We can't annotate the binding either, it's an opaque type!

Rust code
use std::future::Future;

fn main() {
    let async_block: impl Future<Output = Result<u64, std::convert::Infallible>> = async {
        let x: u64 = 1337;
        Ok(x)
    };
}
Shell session
$ cargo check --quiet
error[E0562]: `impl Trait` only allowed in function and inherent method return types, not in variable binding
 --> src/main.rs:4:22
  |
4 |     let async_block: impl Future<Output = Result<u64, std::convert::Infallible>> = async {
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0562`.
error: could not compile `ascription` due to previous error

...that's illegal.

We can't even use type ascription (which is being phased out in its current form anyway), for the same reason:

Rust code
#![feature(type_ascription)]

use std::future::Future;

fn main() {
    let async_block = async {
        let x: u64 = 1337;
        Ok(x)
    }: impl Future<Output = Result<u64, std::convert::Infallible>>;
}
Shell session
$ cargo check --quiet
error[E0562]: `impl Trait` only allowed in function and inherent method return types, not in type
 --> src/main.rs:9:8
  |
9 |     }: impl Future<Output = Result<u64, std::convert::Infallible>>;
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0562`.
error: could not compile `ascription` due to previous error

...we can, however, use TAIT (type alias impl trait):

Rust code
#![feature(type_alias_impl_trait)]

use std::future::Future;

fn main() {
    type AsyncBlock = impl Future<Output = Result<u64, std::convert::Infallible>>;
    let async_block: AsyncBlock = async {
        let x: u64 = 1337;
        Ok(x)
    };
}

...but that's not stable either.

...not that that's stopped you before.

...in fact, can we use TAIT and get the compiler to infer the Ok variant type for us? Let's find out!

Rust code
#![feature(type_alias_impl_trait)]

use std::future::Future;

fn main() {
    type AsyncBlock<T> = impl Future<Output = Result<T, std::convert::Infallible>>;
    let async_block: AsyncBlock<_> = async {
        let x: u64 = 1337;
        Ok(x)
    };
}
Shell session
$ cargo check --quiet
error: non-defining opaque type use in defining scope
  --> src/main.rs:7:38
   |
7  |       let async_block: AsyncBlock<_> = async {
   |  ______________________________________^
8  | |         let x: u64 = 1337;
9  | |         Ok(x)
10 | |     };
   | |_____^
   |
note: used non-generic type `u64` for generic parameter
  --> src/main.rs:6:21
   |
6  |     type AsyncBlock<T> = impl Future<Output = Result<T, std::convert::Infallible>>;
   |                     ^

error: could not compile `ascription` due to previous error

Haha, no. Never seen that error before though, score!

Is this... is this a game to you?

Listen bear, if you can't have fun doing what you love, are you really living?

Anyway, to come back to stable-Rust land, this is the trick async-trait uses:

Rust code
fn main() {
    let async_block = async {
        if let Some(r) = None::<Result<_, std::convert::Infallible>> {
            return r;
        }

        let x: u64 = 1337;
        Ok(x)
    };
}

Which reminds me...

Same trick, but with the async_stream crate

...remember the livereload endpoint we discussed earlier?

Rust code
#[axum::debug_handler(state = crate::serve::ServerState)]
async fn livereload(State(state): State<ServerState>) -> HttpResult {
    fn make_stream(state: &ServerState) -> impl Stream<Item = Result<sse::Event, Infallible>> {
        let mut rx = state.broadcast_rev.subscribe();
        async_stream::try_stream! {
            yield sse::Event::default().event("message").data("Live reloading enabled");

            while let Ok(rev_id) = rx.recv().await {
                yield sse::Event::default().event("new-revision").data(rev_id);
            }
        }
    }

    Sse::new(make_stream(&state))
        .keep_alive(KeepAlive::default())
        .into_http()
}

I said we had to make a separate make_stream function precisely because we couldn't specify the return type...

Could we use the same trick?

Rust code
#[axum::debug_handler(state = crate::serve::ServerState)]
async fn livereload(State(state): State<ServerState>) -> HttpResult {
    let mut rx = state.broadcast_rev.subscribe();
    let stream = async_stream::try_stream! {
        if let Some(r) = None::<Result<_, Infallible>> {
            return r;
        }

        yield sse::Event::default().event("message").data("Live reloading enabled");

        while let Ok(rev_id) = rx.recv().await {
            yield sse::Event::default().event("new-revision").data(rev_id);
        }
    };

    Sse::new(stream)
        .keep_alive(KeepAlive::default())
        .into_http()
}

Turns out — we can't. I'm not sure why, but there's an additional level of macro trickery going on there. Maybe if we use stream! instead of try_stream!, for 50% less magic?

Rust code
#[axum::debug_handler(state = crate::serve::ServerState)]
async fn livereload(State(state): State<ServerState>) -> HttpResult {
    let mut rx = state.broadcast_rev.subscribe();
    let stream = async_stream::stream! {
        if let Some(r) = None::<Result<_, Infallible>> {
            yield r;
        }

        yield Ok(sse::Event::default().event("message").data("Live reloading enabled"));

        while let Ok(rev_id) = rx.recv().await {
            yield Ok(sse::Event::default().event("new-revision").data(rev_id));
        }
    };

    Sse::new(stream)
        .keep_alive(KeepAlive::default())
        .into_http()
}

Yes! That does work!

That's silly though — the stream isn't fallible at all at this point, why not just map it?

Great idea, that seems cleaner:

Rust code
#[axum::debug_handler(state = crate::serve::ServerState)]
async fn livereload(State(state): State<ServerState>) -> HttpResult {
    let mut rx = state.broadcast_rev.subscribe();
    let stream = async_stream::stream! {
        yield sse::Event::default().event("message").data("Live reloading enabled");

        while let Ok(rev_id) = rx.recv().await {
            yield sse::Event::default().event("new-revision").data(rev_id);
        }
    };

    Sse::new(stream.map(Ok::<_, Infallible>))
        .keep_alive(KeepAlive::default())
        .into_http()
}

Boom.

Nice. What were we doing again?

Ah right, the async-trait crate! Now that we've got it working, maybe we can just use the nightly feature instead? Let's find out!

Rust's (incomplete) async_fn_in_trait feature

Trait definition:

Rust code
// yes mom, I know
#![allow(incomplete_features)]
#![feature(async_fn_in_trait)]

/// Something that can refresh credentials
pub trait CredentialsRefresher {
    async fn refresh(&self, creds: &FutileCredentials) -> eyre::Result<FutileCredentials>;
}

Trait impl:

Rust code
impl CredentialsRefresher for ServerState {
    async fn refresh(&self, creds: &FutileCredentials) -> eyre::Result<FutileCredentials> {
        let patreon_id = // etc.

        // exact same code

        Ok(fut_creds)
    }
}

And that works just as well!

I really I had something cooler to show you here, but really, it just works, there were no code changes required from the async_trait version besides removing the proc macro attribute.

One glorious day that feature will be ready and everyone will be able to give up their dependency on the async_trait crate.

...after they bump their MSRV, yeah 😭

Yeah! And if they don't recurse... unless that's somehow solved by then, too.

Wait, what's that about recursion?

Well, you know how async functions are actually just state machines?

Yeah?

Well:

Rust code
async fn do_stuff(x: u32) {
    if x == 0 {
        println!("we did it!")
    } else {
        do_stuff(x - 1).await
    }
}

#[tokio::main]
async fn main() {
    do_stuff(12).await;
}
Shell session
$ Compiling playground v0.0.1 (/playground)
error[E0733]: recursion in an `async fn` requires boxing
 --> src/main.rs:2:27
  |
2 | async fn do_stuff(x: u32) {
  |                           ^ recursive `async fn`
  |
  = note: a recursive `async fn` must be rewritten to return a boxed `dyn Future`
  = note: consider using the `async_recursion` crate: https://crates.io/crates/async_recursion

For more information about this error, try `rustc --explain E0733`.
error: could not compile `playground` due to previous error

Wow, rustc tells you to use a crate?

Yeah, this does it:

Rust code
#[async_recursion::async_recursion]
async fn do_stuff(x: u32) {
    if x == 0 {
        println!("we did it!")
    } else {
        do_stuff(x - 1).await
    }
}

#[tokio::main]
async fn main() {
    do_stuff(12).await;
}

It expands to what you'd expect:

Rust code
// Recursive expansion of async_recursion! macro
// ==============================================

fn do_stuff(
    x: u32,
) -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send>> {
    Box::pin(async move {
        if x == 0 {
            println!("we did it!")
        } else {
            do_stuff(x - 1).await
        }
    })
}

Things get even more fun if you start mixing those two problem areas.

This, for example, works:

Rust code
#[async_trait::async_trait]
trait DoStuff {
    async fn do_stuff(&self);
}

#[async_trait::async_trait]
impl DoStuff for u32 {
    async fn do_stuff(&self) {
        if *self == 0 {
            println!("we did it!")
        } else {
            (self - 1).do_stuff().await
        }
    }
}

#[tokio::main]
async fn main() {
    10.do_stuff().await;
}
Shell session
$ cargo run --quiet
we did it!

But this doesn't:

Rust code
#![allow(incomplete_features)]
#![feature(async_fn_in_trait)]

trait DoStuff {
    async fn do_stuff(&self);
}

impl DoStuff for u32 {
    async fn do_stuff(&self) {
        if *self == 0 {
            println!("we did it!")
        } else {
            (self - 1).do_stuff().await
        }
    }
}

#[tokio::main]
async fn main() {
    10.do_stuff().await;
}
Shell session
$ cargo check
    Checking ascription v0.1.0 (/home/amos/bearcove/ascription)
error[E0733]: recursion in an `async fn` requires boxing
 --> src/main.rs:9:30
  |
9 |     async fn do_stuff(&self) {
  |                              ^ recursive `async fn`
  |
  = note: a recursive `async fn` must be rewritten to return a boxed `dyn Future`
  = note: consider using the `async_recursion` crate: https://crates.io/crates/async_recursion

For more information about this error, try `rustc --explain E0733`.
error: could not compile `ascription` due to previous error

...and we cannot get out of it with the async_recursion crate!

Rust code
#![allow(incomplete_features)]
#![feature(async_fn_in_trait)]

trait DoStuff {
    async fn do_stuff(&self);
}

impl DoStuff for u32 {
    #[async_recursion::async_recursion]
    async fn do_stuff(&self) {
        if *self == 0 {
            println!("we did it!")
        } else {
            (self - 1).do_stuff().await
        }
    }
}

#[tokio::main]
async fn main() {
    10.do_stuff().await;
}
Shell session
$ RUSTFLAGS="-Z macro-backtrace" cargo check
Checking ascription v0.1.0 (/home/amos/bearcove/ascription)
error[E0195]: lifetime parameters or bounds on method `do_stuff` do not match the trait declaration
  --> src/main.rs:9:5
   |
5  |     async fn do_stuff(&self);
   |                      - lifetimes in impl do not match this method in trait
...
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     lifetimes do not match method in trait
   |     in this procedural macro expansion
   |
  ::: /home/amos/.cargo/registry/src/github.com-1ecc6299db9ec823/async-recursion-1.0.0/src/lib.rs:96:1
   |
96 | pub fn async_recursion(args: TokenStream, input: TokenStream) -> TokenStream {
   | ---------------------------------------------------------------------------- in this expansion of `#[async_recursion::async_recursion]`

error[E0308]: method not compatible with trait
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     lifetime mismatch
   |     in this procedural macro expansion
   |
  ::: /home/amos/.cargo/registry/src/github.com-1ecc6299db9ec823/async-recursion-1.0.0/src/lib.rs:96:1
   |
96 | pub fn async_recursion(args: TokenStream, input: TokenStream) -> TokenStream {
   | ---------------------------------------------------------------------------- in this expansion of `#[async_recursion::async_recursion]`
   |
   = note: expected fn pointer `fn(&u32) -> Pin<_>`
              found fn pointer `fn(&'life_self u32) -> Pin<_>`
note: the anonymous lifetime as defined here...
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime `'life_self` as defined here
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ in this procedural macro expansion
   |
  ::: /home/amos/.cargo/registry/src/github.com-1ecc6299db9ec823/async-recursion-1.0.0/src/lib.rs:96:1
   |
96 | pub fn async_recursion(args: TokenStream, input: TokenStream) -> TokenStream {
   | ---------------------------------------------------------------------------- in this expansion of `#[async_recursion::async_recursion]`

error[E0308]: method not compatible with trait
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     lifetime mismatch
   |     in this procedural macro expansion
   |
  ::: /home/amos/.cargo/registry/src/github.com-1ecc6299db9ec823/async-recursion-1.0.0/src/lib.rs:96:1
   |
96 | pub fn async_recursion(args: TokenStream, input: TokenStream) -> TokenStream {
   | ---------------------------------------------------------------------------- in this expansion of `#[async_recursion::async_recursion]`
   |
   = note: expected fn pointer `fn(&u32) -> Pin<_>`
              found fn pointer `fn(&'life_self u32) -> Pin<_>`
note: the lifetime `'life_self` as defined here...
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ in this procedural macro expansion
   |
  ::: /home/amos/.cargo/registry/src/github.com-1ecc6299db9ec823/async-recursion-1.0.0/src/lib.rs:96:1
   |
96 | pub fn async_recursion(args: TokenStream, input: TokenStream) -> TokenStream {
   | ---------------------------------------------------------------------------- in this expansion of `#[async_recursion::async_recursion]`
note: ...does not necessarily outlive the anonymous lifetime as defined here
  --> src/main.rs:9:5
   |
9  |     #[async_recursion::async_recursion]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Some errors have detailed explanations: E0195, E0308.
For more information about an error, try `rustc --explain E0195`.
error: could not compile `ascription` due to 3 previous errors
Cool bear's hot tip

The diagnostics shown here are very verbose, but that's a good thing here, enabled via -Z macro-backtrace, another goodie that's nightly-only.

Well, that's not entirely true... we can get away with it, if we get a little creative:

Rust code
#![allow(incomplete_features)]
#![feature(async_fn_in_trait)]

trait DoStuff {
    async fn do_stuff(&self);
}

impl DoStuff for u32 {
    async fn do_stuff(&self) {
        #[async_recursion::async_recursion]
        async fn do_stuff_boxed(u: u32) {
            if u == 0 {
                println!("we did it!")
            } else {
                do_stuff_boxed(u - 1).await
            }
        }

        do_stuff_boxed(*self).await
    }
}

#[tokio::main]
async fn main() {
    10.do_stuff().await;
}

Anyway: I've tasted the future, and now I want more. Why does this keep happening?