The last week or two has been exciting in Rust async land. We're making great progress on one of the open questions around async functions in traits, and I think we're close to being ready to propose something officially. In this post, I'd like to describe the proposal and discuss some of the tradeoffs and open questions with it.

We've had a couple of ideas going around so far. One of the main ones is Return Type Notation (RTN), which Niko describes in his recent post. In my last post, I suggested that we could infer the necessary bounds in many cases.

While I was excited about inferring bounds at first, one major shortcoming is that it creates new semantic versioning hazards. The inference depends on the body of the the function you've annotated, which means when modifying the function you could easily add or remove bounds from the signature by accident.

In the discussions we've had since then, we have been converging on a solution that we expect will work in the common cases, but avoids both the verbosity inherent in RTN and the semver hazards with inferring bounds. This is the solution I'll be describing in this post.

Recap: Two Versions of the Send Bound ProblemπŸ”—

One of the things I've realized is that there are two variants of the Send Bound Problem. I'll call these the Promise and Require variants.

The Promise variant is "how can I promise that my async function will always return a Send future?" There are several subvariants. We may want to define an async trait so that all implementors must always have Send implementations. This is what the #[async_trait] macro does by default. Or, even if the trait does not require it, we may want to make this promise in out implementation. And finally, for just a bare async fn, we may want to be able to make the same promise.

The Require variant is "how can I require that the implementation I'm given can be used in a Send context?" This is looking at the use side rather than the definition side. Let's recall the do_health_check example:

fn do_health_check<H>(mut health_check: H, server: Server)
where
    H: HealthCheck + Send + 'static
{
    spawn(async move {
        health_check.check(server).await;
    });
}

We want to make sure that, although the HealthCheck trait does not require that implementations return Send futures, we only can call do_health_check with those that do so that we can spawn the background task.

My feeling so far has been that the Promise variant is the easier one to solve, so it is easy to accidentally start talking about that one, while the Require variant is the more important problem. In this post, I will only be talking about the Require variant, although I suspect the proposal may generalize to the Promise variant and I may speculate on that.

The ProposalπŸ”—

Without further ado, here is the proposal.

First, we require async traits to be declared as such. This means the HealthCheck trait we've talking about gains an additional async keyword:

trait async HealthCheck {
    async fn check(&mut self, server: Server);
}

For the most part, we can think of this new async as becoming part of the name of the trait. It's no longer just HealthCheck, but async HealthCheck. Declaring a trait with async means the trait is allowed to have async methods.1

Because we've changed the name of HealthCheck, we have to change where we use the trait as well:

fn do_health_check<H>(mut health_check: H, server: Server)
where
    H: async HealthCheck + Send + 'static
{
    spawn(async move {
        health_check.check(server).await;
    });
}

This new async keyword in the where clause does a couple of things. First, it's a hint that the trait has async methods. More importantly, it gives us a place to hang additional bounds if needed. Because we are spawning a future that awaits calls from this trait, we need a Send bound.2 So, to notate this, we'd use async(Send) in the bound:

fn do_health_check<H>(mut health_check: H, server: Server)
where
    H: async(Send) HealthCheck + Send + 'static
{
    spawn(async move {
        health_check.check(server).await;
    });
}

The trait name async(Send) HealthCheck would mean the HealthCheck trait, with async methods, all of which have a Send bound on their returned futures.

So that's the proposal in a nutshell.

One thing I'd like to point out is that although so far we've only talked about Send bounds, I'm imagining that the grammar would allow any bound on the async keyword (although it might make sense to limit it to auto traits). For example, one could imagine writing:

fn foo<T>(x: T)
where
    T: async(Send + Clone + Debug) MyTrait
{
    ...
}

In practice, it's probably hard to implement Debug on the future returned by an async method...3

DiscussionπŸ”—

There's a lot I like about this proposal.

It's relatively lightweight syntactically, but we assume it's powerful enough to meet the common cases. To be honest, we don't actually know how common it will be that users want to have some methods that are Send but some that are not. The fact that #[async_trait] works well suggests that the all or nothing approach should be fine in many cases. If there are cases that users need to be more precise, however, we can still provide return type notation for those advanced use cases.

The semantics of these new bounds seems easy to explain. We don't have to talk about looking at function bodies and we definitely don't have to mention anything about flow-sensitivity, while we might if we did something that relied on more inference. This helps keep Rust explicit and predictable as a language, without being burdensome.

This proposal also dovetails nicely with several others that are currently in progress, and is immediately open to more generalizations. The syntax we've introduced here actually largely comes from the keyword generics initiative. Although we have not talked about maybe-async bounds (written ?async), the syntax is completely consistent with what we've seen here.

I know I said I wasn't going to talk about the Promise variant of the Send bounds problem, but it's a relatively small change from what we have here to also allow trait modifiers anywhere else the async keyword is allowed. For example, we could write the following to declare an async function whose returned future is guaranteed to be Send:

async(Send) fn foo() -> i32 { ... }

That said, I think there may be better ways to solve this problem,4 so I don't want to dwell to much on this just yet.

Open QuestionsπŸ”—

We still have a few open questions though. I'll briefly touch on these here.

Should methods should be Send by default? Either way is feasible. For example, methods could be Send by default and you could use async(?Send) to opt out, or they could be not assumed to be Send by default and you use async(Send) to opt in. There are arguments for both, but fortunately it's a relatively minor detail and is easy to go either way.

How does this interact with supertraits and trait aliases? We have some time to figure this out for aliases, since trait aliases aren't a thing yet. For supertraits, we can probably start with a more conservative option and relax it later if needed.

Are non-async and async traits in the same namespace? This question gets at whether you could define both trait Foo { ... } and trait async Foo { ... } in the same module. While I won't do so in this post, this has enough implications that it's probably worth spending some time on. For example, if we get this one wrong, we might end up in a situation where users have to write async AsyncFoo, which would just be sad.

ConclusionπŸ”—

So anyway, that's the proposal. I want to give a big shout out to everyone in the Async Working Group, and Yosh Wuyts in particular since he largely came up with the final syntax presented here in conjunction with his work on keyword generics. Also, thanks to Nick Cameron for his early feedback on this post. The proposal presented in this post incorporates a lot of ideas from many different people, and it's really great to see everyone's input coming together towards a solution we can be happy about. We seem to be at a point where we've struck a nice balance for ergonomics, utility, and predictability. Of course, the best way to know for sure is to prototype something and play around with it!

I'm excited to see progress in this area and am eager to see async functions in traits become fully supported in Rust!


1

Technically, you wouldn't be required to have async methods, but we'd probably want to add a lint warning about unnecessary async keywords, just like we do for mut.↩

2

This is assuming we're using a multithreaded executor like Tokio. The spawn function from single threaded executors, like many embedded async runtimes provide, would likely not require a Send bound.↩

3

This potentially opens up some really powerful features though. For example, one could imagine futures that implement serde::Serialize and serde::Deserialize to make futures that can move not just between threads but between nodes in a cluster, or web frameworks where you can await input from the client.↩

4

One example suggested by Josh Triplett is if you could explicitly refer to the return type in where clauses. Then you could say async fn foo() -> i32 where return: Send { ... }. This makes the scoping a little clearer around parameters (for example, in async(Send + 'a) fn foo<'a>() -> i32 { ... }, it'd be weird to refer to 'a before it's declared), but it also is less clear as to whether we are saying i32 is Send or that the hidden future that async fn desugars to is Send.↩