dtolnay / async-trait

Type erasure for async trait methods
Apache License 2.0
1.84k stars 85 forks source link

Methods created are `fn (&Struct) -> ...`, rather than `for<'_> fn (&Struct) -> ...` #133

Closed ids1024 closed 3 years ago

ids1024 commented 4 years ago

I don't understand the sorcery of HRTBs and async well enough to know if there's a major difficulty with this, but it seems to be relevant when trying to accept the function as an argument.

Consider this:

use async_trait::async_trait;

struct Struct;

#[async_trait]
trait Trait {
    async fn g(&self);

    fn h(&self);
}

impl Struct {
    async fn f(&self) {}
}

#[async_trait]
impl Trait for Struct {
    async fn g(&self) {}

    fn h(&self) {}
}

fn main() {
    let _: () = Struct::f;
    let _: () = Struct::g;
    let _: () = Struct::h;
}

This tells us Struct::f has type for<'_> fn(&Struct) -> impl Future {Struct::f}. Struct::h has type for<'r> fn(&'r Struct) {<Struct as Trait>::h}.

But Struct::g is fn(&Struct) -> Pin<Box<dyn Future<Output = ()> + Send>> {<Struct as Trait>::g::<'_, '_>}.

If this can't be changed to have a for<'_> bound, I guess that should be documented as one of the crate's limitations...

ids1024 commented 4 years ago

Looking at how the macro expands, it seems the for<'_> part is present with g1, but not g2:

    fn g1<'life0>(&'life0 self);

    fn g2<'life0, 'async_trait>(&'life0 self)
        where 'life0: 'async_trait;

I guess Rust introduces this for<'_> depending on the lifetimes involved in the function, but since this crate is partially bypassing Rust's lifetime inference with explicit lifetime bounds, it can't do that here.

ids1024 commented 4 years ago

In particular, it seems it's the 'life0: 'async_trait bound that's responsible, though I don't know if there's a good alternative that will work...

Edit: This may be a limitation of the compiler.

ids1024 commented 4 years ago

I guess the reason this works without async-trait is based on how lifetime elision works:

If there is exactly one lifetime used in the parameters (elided or not), that lifetime is assigned to all elided output lifetimes.

So with the async fn outside the trait, the return value's lifetime is inferred to be the same as the parameters.

I guess the "correct" solution would be for async-trait to apply all the lifetime elision rules the compiler does, but that isn't likely to be simple, I guess...

ids1024 commented 3 years ago

It seems this is just an inconsistency in how Rustc prints the types. There was a real issue I was having, but as far as I can tell it's ultimately due to limits in Rustc.