Skip to content

Instantly share code, notes, and snippets.

@withoutboats
Last active May 26, 2018 17:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save withoutboats/2914367cc7b053ca840b8b2c0bcf840c to your computer and use it in GitHub Desktop.
Save withoutboats/2914367cc7b053ca840b8b2c0bcf840c to your computer and use it in GitHub Desktop.
async methods

Goal:

trait MyTrait {
    async fn foo(&self) -> i32;
}

impl MyTrait for MyType {
    async fn foo(&self) -> i32 {
       /* ... * /
    }
}

Problems

Several of these problems apply equally well to impl Trait in traits.

Requires GATs to implement

An async fn returns an impl Future. In a trait, this would be desugared to anonymous associated type:

trait MyTrait {
    type Future: Future<Output = i32>;
    fn foo(&self) -> Self::Future;
}

However, they capture all input lifetimes, therefore the associated type needs to be generic over lifetimes:

trait MyTrait {
   type Future<'a>: Future<Output = i32>;
   fn foo<'a>(&'a self) -> Self::Future<'a>;
}

Design work on GATs is complete, but they are not implemented. First, the chalk trait implementation needs to be integrated into rustc. Work on that is underway.

Bounding the return future

The returned future of the async function will implement Future, but what about other traits? By virtue of the design of async functions, in the concrete case it will "leak" auto traits like Send and Sync. But in the generic case, its not obvious how to require that it implement Send or Sync. This comes up in two cases:

  • How can I define a trait so that an async method's future must be Send or Sync?
  • How can I take a T: Trait and require that its async method's future must be Send or Sync?

For impl Trait, the proposed solution is for impl Trait to be disallowed in trait definitions, and instead only allowed in implementations. The trait definitions would use an "explicit" form. Applied to async fn, the original example would look like this:

trait MyTrait {
    type Future<'a>: Future<Output = i32>;
    fn foo<'a>(&'a self) -> Self::Future<'a>;
}

impl MyTrait for MyType {
    abstract type Future<'a>; // The type definition of the future assoc type should
                              // be inferred, because we said it was 'abstract'

    // This matches the signature.
    async fn foo(&self) -> i32 {
       /* ... * /
    }
}

Users can then require Send using bounds on the associated type, either in the trait definition or in a bound on a generic parameter (e.g. T: MyTrait, T::Future: Send).

While this seems like a possibly workable solution for impl Trait, something about it is very unsatisfactory for async fn because of the way it spills the guts of the feature it is trying to hide.

An alternative design would be to find a way to write bounds on the future returned by an async fn directly, while preserving async fn syntax. Hypothetical examples:

trait MyTrait {

    #[async_bounds(Send)]
    async fn foo(&self) -> i32;
}

fn bar<T: MyTrait>(x: T) where
    async T::foo: Send,
{ /* ... */ }

(This is just one syntax, others are certainly plausible).

Future initializer state

Consider the Service trait:

trait Service {
    type Request;
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;
    fn call(&mut self, req: Self::Request) -> Self::Future;
}

It might be nice to transform this to an async fn:

trait Service {
    type Request;
    type Response;
    type Error;
    async fn call(&mut self, req: Self::Request) -> Result<Self::Response, Self::Error>;
}

However, this changes the meaning, because the returned future now captures the lifetime of &mut self.

As it stands, the Self type is used as a factory to initialize futures, but does not contain state that is used by those futures during their execution.

Ultimately, this pattern is not well supported by async fn in traits. There is no definition that supports both patterns, because they have different implications: in the "initializer" pattern, the state is no longer held while the future is executing, whereas in the pattern encoded in async fn, it is. This means ultimately any trait has to choose between these two options, and there isn't a trait definition that is more flexible.

For this reason, this doesn't seem like a "problem": traits that act as "future factories," rather than stateful objects with asynchronous methods, will need to use the associated type (or possible impl Future) syntax, not async fn.

@jsgf
Copy link

jsgf commented May 26, 2018

Can traits with anonymous associated types/async methods be object safe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment