Async/Await V: Getting back to the futures
Two posts ago I proposed a particular interface for shipping self-referential generators this year. Immediately after that, eddyb showed me a better interface, which I described in the next post. Now, to tie everything together, its time to talk about how we can integrate this into the futures ecosystem.
Starting point: this Generator API
To begin, I want to document the generator API I’ll be using in this post, which is roughly what followed from my previous post:
trait StableGenerator {
type Yield;
type Return;
fn resume_stable(self: Anchor<&mut Self>) -> CoResult<Self::Yield, Self::Return>;
}
trait Generator: StableGenerator {
fn resume(&mut self) -> CoResult<Self::Yield, Self::Return>;
}
Once again, we have two generator types, both of which have stable APIs. The first requires that the generator never move once you start calling resume, whereas the second doesn’t.
Constraining the problem
All async function are going to return either a Generator
or a
StableGenerator
, possibly wrapped in a newtype. Ideally, this type would
implement Future
in some way - it would be able to possible to call
combinators on it, await it, and put it on an event loop.
However, the core method of Future
is poll
, which does not restrict the
type from being moved between calls to poll. For generators which need to be
held stable, we cannot just implement Future
on the generator - we can only
implement it on an anchored generator. But we don’t want to anchor the
generator before we pass it to a combinator, since this will probably require
us to heap allocate it.
On the other hand, we anticipate that we will need to heap allocate the future before putting it on the event pool. The event loop is expected to be able to dynamically dispatch between multiple types of futures. Our goal is not to eliminate all heap allocations, but collapse them into the single allocation we use when putting the future on the event loop.
These are the things we need:
- A way to apply
await
to a stable generator without heap allocation. - A way to apply a combinator to a stable generator without heap allocation.
Solving await
Await has the semantics of consuming a future and then continuously polling it, yielding when its not ready. Today, it expands something like this:
// This
await!(future)
// Expands like this
loop {
match future.poll() {
Ok(Async::NotReady) => yield,
Ok(Async::Ready(data)) => Ok(data),
Err(error) => Err(error),
}
}
Importantly, once you await a future, you can’t do anything with it again. This gives us the leeway to anchor the future without changing semantics:
await!(future);
loop {
let future = Anchor::pinned(&mut pin(future));
match future.poll() {
Ok(Async::NotReady) => yield,
Ok(Async::Ready(data)) => Ok(data),
Err(error) => Err(error),
}
}
Solving combinators
Combinators are trickier: you actually are moving the future around when you use them. We need to make sure that you don’t poll a generator, then move it into a combinator after the fact, while still letting you apply combinators to the generator before you start polling it.
An opening arrives in the form of a recently planned change to
futures - all of the combinators are being moved to a trait
called FutureExt
. The currently envisioned API of futures looks like this:
pub trait Future {
type Item;
type Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error>;
}
pub trait FutureExt {
// All the combinators are here
}
impl<T: Future> FutureExt for T {
// ...
}
However, we can introduce a hitch into this by adding a third trait, analogous
to the StableGenerator
trait above:
pub trait StableFuture {
type Item;
type Error;
fn poll_stable(self: Anchor<&mut Self>) -> Poll<Self::Item, Self::Error>;
}
impl<T: StableFuture + ?Sized> Future for Anchor<Box<T> {
type Item = T::Item;
type Error = T::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
self.as_mut().poll_stable()
}
}
Now that we have this available, we can change the impls like so:
// Any Future can be a StableFuture
impl<T: Future + ?Sized> StableFuture for T {
type Item = <T as Future>::Item;
type Error = <T as Future>::Error;
fn poll_stable(self: Anchor<&mut Self>) -> Poll<Self::Item, Self::Error> {
T::poll(unsafe { Anchor::get_mut(self) })
}
}
// Because all Futures are StableFutures, this blanket impl is strictly broader
// than the previous blanket impl of FutureExt for T: Future.
impl<T: FutureStable> FutureExt for T {
// ...
}
Tada! Now you can call combinators on any future, whether it has to be kept stable while polling or not.
Caveat: some combinators are not stable-compatible
However, there are actually some combinators which - as a part of their API -
move their underlying future after polling it. These combinators are not
compatible with StableFuture
, and need to also be bound Future
. The most
important of these is the current select combinator, which evaluates to the
result of one of its futures as well as the other unfinished future, allowing
you to keep polling it. This API is inherently incompatible with unanchored
immovable futures, and we’ll need to decide what to do about that.
Combinators like this just have to be bound where Self: Future
and they can
still live in FutureExt
.
Conclusion
So at this point we’ve built up exactly the abstraction we wanted to have:
- We can support self-referential generators with a safe API.
- We can await a self-referential future without allocating it.
- We can apply combinators to a self-referential future without allocating it.
- We only need to allocate once, when putting the future onto the event loop.
This is not only performant, it is also significantly more ergonomic than the current API of futures. No longer do you need to reference count data which you want to use before and after a yield point. This solves some of the most important pain points in the futures ecosystem.
But there are still many questions left to resolve before we get to stable. The names and hierarchies of the APIs discussed in this series are a total open question, and there are several slight variants on them which we need to decide the trade offs between. We haven’t even tried to address the syntax of generators and async functions in all of their variations; this remains a wide set of open questions that need to be resolved before stabilization.
Over the next few weeks we’ll continue to explore these questions, possibly in a continuation of this blog series or possibly in other ways. I hope to have some RFCs relating to this subject ready by early March.