[Idea] Object safe async trait methods

Here's an idea for how to implement object-safe async trait method.

// trait definition will stay like this, and this is obviously object-safe
trait AsyncTrait {
    type Output;
    type Future<'a>: Future<Output = Self::Output> + 'a;
    
    fn do_work(&mut self) -> Self::Future<'_>;
}

// but implementations may use async methods where profitable
struct SomeType;

impl AsyncTrait for SomeType {
    type Output = ();
    // relies on impl trait syntax here, which should resolve to the anonymous async object
    type Future<'a> = impl Future<Output = Self::Output> + 'a;
    
    // this is allowed because it should just desugar to `fn do_work(&mut self) -> impl Future<Output = ()>` which matches the trait signature
    async fn do_work(&mut self) {
        // do some work
    }
}

Now the challenge is to create a trait object from this. Well, we could just create a wrapper type

struct BoxedAsyncTraitFuture<T>(pub T);

impl<T: AsyncTrait> AsyncTrait for BoxedAsyncTraitFuture<T> {
    type Output = T::Output;
    // a single named type
    type Future<'a> = PIn<Box<dyn Future<Output = Self::Output> + 'a>>;
    
    fn do_work(&mut self) -> Self::Future<'_> {
        Box::pin(self.0.do_work())
    }
}

let _boxed_async_trait: Box<dyn AsyncTrait<Output = (), Future = PIn<Box<dyn Future<Output = ()>>>>> = Box::new(BoxedAsyncTraitFuture(SomeType));

_boxed_async_trait.do_work().await

Nothing really has to be automatic beside that impl trait bit. However this does rely on two other nightly features.

One upside is that the wrapper BoxedAsyncTraitFuture could be implemented in a different way which uses a completely different pointer type if you wanted and this requires no real language magic. Only features which are already in nightly:

  • GATs which are necessary for async trait methods anyways
  • impl trait in associated types which seems natural

edit: just trying this out on playground, and it looks like GATs blocks trait objects :slightly_frowning_face:, so one feature that's not yet on nightly but could be since lifetime GATs should be allowed in trait objects.

I was going to comment on this more but ended up filling an issue instead.

But yes, this pattern of "lifting" to an object-safe type-erased impl is fairly standard and is the pattern used by e.g. erased-serde. Doing the lifting explicitly rather than implicitly is maybe desirable but there is value in making dyn AsyncTrait "just work".

I expect that attaching the automatic lifting glue to "dyn trait" would make sense.

#![feature(generic_associated_types_extended)], but it's incomplete, unsound, and much more just "lift some restrictions" than any amount of thought out.