Motivation
The async fn RFC (in the process of being implemented) has two limitations:
- It only supports free async functions, not async trait methods (which allows async functions to play a role in our polymorphism system).
- It does not provide a way to require that your async fn’s future implements a trait like
Send
. This can often be very important.
These two problems were sort of related. This pre-RFC is a sketch of how they could be solved.
The main insight is that we can take inspiration from tuple structs, which create both a type and a function, and do the same thing for async functions.
Design
async fn
creates a function and an abstract type
Today, async fn
desugars to a function that returns an impl Trait
. This means that async fn foo
creates only an item in the expression namespace, not an item in the type namespace. Most kinds of item definitions in Rust only create a new item in 1 name space, but there is an exception: tuple structs, which create both a type and an expression - the expression is the constructor function for the type.
We could make async fn
work similarly to tuple struct: create both an expression & and a type, the expression being the constructor for the type. In this case, the type is the future returned by async fn
. This type would be an abstract/existential type. So instead of desugaring like this:
async fn foo() -> i32
/* desugars to: */
fn foo() -> impl Future<Output = i32>
We desugar it like this:
async fn foo() -> i32
/* desugars to: */
abstract type foo: Future<Output = i32>;
fn foo() -> foo
This allows the name foo
to be used in type contexts to refer to the future that is constructed by the foo
async fn. For example:
where foo: Send // bound that the future returned by foo implements Send
Async methods
Similarly, async methods desugar to an associated type and a method:
trait Trait {
async fn foo(&self) -> i32;
}
/* desugars to: */
trait Trait {
type foo<'a>: Future<Output = i32>;
fn foo(&self) -> Self::foo<'_>;
}
There’s an important thing to notice here: the returned future captures all input lifetimes, therefore it needs to be a generic associated type. So async methods cannot be implemented until the compiler can comprehend them. (chalk can now, so its a matter of importing the concepts from chalk into the rustc type system.)
This allows downstream uses to require that the future returned by an async function implement a trait:
fn bar<T: Trait>(arg: &T) where T::foo: Send
Sugar for bounding the return type
While this system works great for adding that bound in a where clause, it is kind of awkward if you want to apply that check in the trait or function definition. Since no longer being Send
would be a breaking change, users are likely going to want to automate the check that their API is Send
if it is. That looks rather gnarly:
trait Trait where
Self::foo: Send // note: must be on the trait, not method
{
async fn foo(&self) -> i32;
}
// weird double reference to `foo` in this header:
async fn foo() -> i32 where foo: Unpin {
// ...
}
As sugar for this, we allow the async
keyword in a function header to take a parenthesized bound:
trait Trait {
async(Send) fn foo(&self) -> i32;
}
async(Unpin) fn foo() -> i32 {
// ...
}
If the trait contains the bound, all implementations must contain at least that bound (they can contain extra traits as well).
This bound is added to the bound on the existential type so that the function is checked that its future implements that bound.
Note: non-autotraits
At least at first, this would only be useful for Send
and Sync
, because they’re the only traits that will leak properly through the Future that the async fn evaluates to. However, this could be extended in the future:
- Similar to closures, we could make the future implement
Copy
andClone
if its entirely captured environment does. - We could interpret
Unpin
specially to typecheck the Future as requiring that it be safe to move (futures without borrows).
However, neither of these are a part of this pre-RFC.
Alternatives
The main alternative is to only allow async fn as sugar in implementations, while requiring trait definitions to provide the whole associated type themselves. This seems very confusing and difficult to learn for users:
trait Trait {
type Future<'a>: Future<Output = i32>;
fn foo(&self) -> Self::Future<'_>;
impl Trait for Type {
// declare the associated type abstract so it will be inferred:
abstract type Future<'a>;
// because this has the right signature, it is allowed even though
// it is syntactically different from the trait definition:
async fn foo(&self) -> i32 { /* ... */ }
}
This doesn’t solve the problem of bounding the return type of free async fns, which would still just desugar to impl Future
.