Async Methods I: generic associated types

Async/await continues to move along swimmingly. We’ve accepted an RFC describing how the async/await syntax will work in Rust, and work is underway on implementing support for it in the compiler. We’re hopeful that users will be able to start experimenting with the syntax on nightly by early July.

The RFC for async/await didn’t address one important thing: async methods. It is very important for people defining libraries to be able to define traits that contain async functions, like this:

trait Foo {
    async fn foo_method(&self) -> i32;
}

Unfortunately, Rust isn’t yet capable of supporting a trait like this. To start this series, let’s dig into what that would require.

Async methods and associated types

When you write an async function, it gets translated internally in the compiler to a future, and a function that returns a future. The “desugared” signature of the function looks like this:

async fn foo() -> i32 { /* ... */ }
// desugars to:
fn foo() -> impl Future<Output = i32> { /* ... */ }

The future that each async function returns is a unique type generated by the compiler for that particular function. This means that if you have two trait impls of the same trait, the return type of the async functions in those two impls is different.

// These two `foo_method`s return distinct types, that both implement Future.
impl Foo for i32 {
    async fn foo_method(&self) -> i32 {
       /* ... */
    }
}

impl Foo for String {
    async fn foo_method(&self) -> i32 {
       /* ... */
    }
}

We have a mechanism for expressing this in Rust: associated types. Just as an Iterator has an Item type, every trait with an async method can be treated as if it has a secret, anonymous associated type for the future returned by that async method.

That is, the Foo trait above is sort of like this:

trait Foo {
    type _Future: Future<Output = i32>;
    fn foo_method(&self) -> Self::_Future;
}

That’s all well and good, but Rust already supports associated types. So what’s stopping us from implementing async methods? The subtle difference is that the secret associated type on Foo is not just an associated type, but a generic associated type.

Generic associated types

Let’s look at the signature of foo_method again:

async fn foo_method<'a>(&'a self) -> i32;

You may notice that this time I have expressed the lifetime of &self explicitly. The way that async/await works, the future returned by an async function captures all lifetimes inputed into the function. So the desugared signature of this method is more like this:

// The returned Future must live at least as long as the reference to self:

fn foo_method<'a>(&'a self) -> impl Future<Output = i32> + 'a;

When we try to write the Foo trait, what we get is something like this:

trait Foo {
    type _Future<'a>: Future<Output = i32> + 'a;
    fn foo_method<'a>(&'a self) -> Self::_Future<'a>;
}

In order to express that lifetime relationship, the Future type needs to be generic over a lifetime, so that it can capture the lifetime from the self argument on the method.

Unforunately, associated types cannot have generic parameters today. This is a long desired feature called generic associated types, or GATs, (I wrote the RFC for this more than two years ago!). Until the rust compiler support GATs, async methods do not work.

There’s good news though: about a year ago, Niko started working on an experimental implementation of the Rust trait system called chalk. chalk already supports GATs, because it was designed from the ground up to be able to. the compiler team is now beginning to do the arduous work of integrating the design ideas from chalk into rustc proper, to improve the trait system in several ways.

That is to say, generic associated types are getting closer every day. Its possible that they will be supported on nightly some time this year, removing the major implementation blocker for nightly.

Next post

So if that’s the big blocker for implementing async methods, and progress is already being made on removing it, what is left to do on the design front? In the next post I’ll explore one of the design fallouts of having an “anonymous” associated type, and what we could do about the problem.