async_fn_in_traits
Support async fn
in traits that can be called via static dispatch. These will desugar to an anonymous associated type.
Async/await allows users to write asynchronous code much easier than they could before. However, it doesn't play nice with other core language features that make Rust the great language it is, like traits.
In this RFC we will begin the process of integrating these two features and smoothing over a wrinkle that async Rust users have been working around since async/await stabilized nearly 3 years ago.
You can write async fn
in traits and trait impls. For example:
trait Service {
async fn request(&self, key: i32) -> Response;
}
struct MyService {
db: Database
}
impl Service for MyService {
async fn request(&self, key: i32) -> Response {
Response {
contents: self.db.query(key).await.to_string()
}
}
}
This is useful for writing generic async code.
Currently, if you use an async fn
in a trait, that trait is not dyn
safe. If you need to use dynamic dispatch combined with async functions, you can use the async-trait
crate. We expect to extend the language to support this use case in the future.
Note that if a function in a trait is written as an async fn
, it must also be written as an async fn
in your implementation of that trait. With the above trait, you could not write this:
impl Service for MyService {
fn request(&self, key: i32) -> impl Future<Output = Response> {
async {
...
}
}
}
Doing so will give you an "expected async fn" error. If you need to do this for some reason, you can use an associated type in the trait:
trait Service {
type RequestFut<'a>: Future<Output = Response>
where
Self: 'a;
fn request(&self, key: i32) -> RequestFut;
}
impl Service for MyService {
type RequestFut<'a> = impl Future + 'a
where
Self: 'a;
fn request<'a>(&'a self, key: i32) -> RequestFut<'a> {
async { ... }
}
}
Note that in the impl we are setting the value of the associated type to impl Future
, because async blocks produce unnameable opaque types. The associated type is also generic over a lifetime 'a
, which allows it to capture the &'a self
reference passed by the caller.
We introduce the async fn
sugar into traits and impls. No changes to the grammar are needed because the Rust grammar already support this construction, but async functions result in compilation errors in later phases of the compiler.
trait Example {
async fn method(&self);
}
impl Example for ExampleType {
async fn method(&self);
}
When an async function is present in a trait or trait impl…
This limitation is expected to be lifted in future RFCs.
async
syntaxIt is not legal to use an async function in a trait and a "desugared" function in an impl.
Async functions in a trait desugar to an associated function that returns a generic associated type (GAT):
fn
, including any implied bounds.$
to represent this name.)trait Example {
async fn method<P0..Pn>(&self)
where
WC0..WCn;
}
// Becomes:
trait Example {
type $<'me, P0..Pn>: Future<Output = ()>
where
WC0..WCn, // Explicit where clauses
Self: 'me; // Implied bound from `&self` parameter
fn method(&self) -> Self::$<'_>
where
WC0..WCn;
}
async fn
that appear in impls are desugared in the same general way as an existing async function, but with some slight differences:
$
is equal to an impl Future
type, rather than the impl Future
being the return type of the functionSelf::$<...>
with all the appropriate generic parametersOtherwise, the desugaring is the same. The body of the function becomes an async move { ... }
block that both (a) captures all parameters and (b) contains the body expression.
impl Example for ExampleType {
async fn method(&self) {
...
}
}
impl Example for ExampleType {
type $<'me, P0..Pn> = impl Future<Output = ()> + 'me
where
WC0..WCn, // Explicit where clauses
Self: 'me; // Implied bound from `&self` parameter
fn method(&self) -> Self::$<'_, P0..Pn> {
async move { ... }
}
}
This RFC represents the least controversial addition to async/await that we could add right now. It was not added before due to limitations in the compiler that have now been lifted – namely, support for Generic Associated Types and Type Alias Impl Trait.
Supporting async fn and dyn is a complex topic – you can read the details on the dyn traits page of the async fundamentals evaluation doc.
Yes, nothing in this RFC precludes us from making traits containing async functions dyn safe, presuming that we can overcome the obstacles inherent in the design space.
Users in the ecosystem have worked around the lack of support for this feature with the async-trait proc macro, which desugars into Box<dyn Future>
s instead of anonymous associated types. This has the disadvantage of requiring users to use Box<dyn>
along with all the performance implications of that, which prevent some use cases. It is also not suitable for users like embassy, which aim to support the "no-std" ecosystem.
The async-trait crate will continue to be useful after this RFC, because it allows traits to remain dyn
-safe. This is a limitation in the current design that we plan to address in the future.
async-trait
crateThe most common way to use async fn
in traits is to use the async-trait
crate. This crate takes a different approach to the one described in this RFC. Async functions are converted into ordinary trait functions that return Box<dyn Future>
rather than using an associated type. This means that the resulting traits are dyn safe and avoids a dependency on generic associated types, but it also has two downsides:
Send
or not. The async-trait
crate defaults to Send
and users write #[async_trait(?Send)]
to disable this default.Since the async function support in this RFC means that traits are not dyn safe, we do not expect it to completely displace uses of the #[async_trait]
crate.
The real-async-trait
lowers async fn
to use GATs and impl Trait, roughly as described in this RFC.
It is not a breaking change for traits to become dyn safe. We expect to make traits with async functions dyn safe, but doing so requires overcoming a number of interesting challenges, as described in the async fundamentals evaluation doc.
The impl trait initiative is expecting to propose "impl trait in traits" (see the explainer for a brief summary). This RFC is compatible with the proposed design.
This RFC does not propose any means to name the future that results from an async fn
. That is expected to be covered in a future RFC from the impl trait initiative; you can read more about the proposed design in the explainer.