Without boats, dreams dry up
In July, I described a way to make pinning more ergonomic by integrating it more
fully into the language. Last week, I develoepd that idea further with the notion of
UnpinCell
: a wrapper type that lets a user take an &pin mut UnpinCell<T>
and produce an &mut T
, similar to how other cells let a user take a shared reference to the cell and produce a mutable
reference to its contents. I believe that this notion can also solve the biggest outstanding issues
facing generators: the fact that the Iterator
interface does not permit
self-referential values.
As I wrote in my explanation of Pin’s design, the biggest advantage that Pin had over other
design ideas was that it was a trivially backward compatible way of introducing a contract that an
object will never be moved. But this meant that a trait could only opt into that contract using the
new interface; traits that existed before Pin and don’t opt into that contract cannot be implemented
by types that have self-referential values. The most problematic trait here is Iterator
, because
generators (functions that evaluate to iterators in the same way async functions evaluate to
futures) would ideally support self-referential values just like async functions do. So long as the
interface for Iterator
takes a mutable reference and not a pinned mutable reference, implementers
must assume the iterator can be moved around and therefore can’t be self-referential.
Last week, Tyler Mandry published an interesting post about a problem that the Rust
project calls “Barbara battles buffered streams.” Tyler does a good job explaining the issue, but
briefly the problem is that the buffering adapters from the futures library (Buffered
and
BufferUnordered
) do not interact well with for await
if the processing in the body is
asynchronous (i.e. if it contains any await
expressions).
I think we can better understand the problem if we examine it visually. First, let’s consider the
control flow that occurs when a user processes a normal, non-asynchronous Iterator
using a for
loop:
┌── SOME ────────────────┐
╔═══════════════╗ ╔═══════▼═══════╗
║ ║▐▌ ║ ║▐▌
──────▶ NEXT ║▐▌ ║ LOOP BODY ║▐▌
║ ║▐▌ ║ ║▐▌
╚════════════▲══╝▐▌ ╚═══════════════╝▐▌
▀▀│▀▀▀▀▀▀▀▀▀│▀▀▀▀▘ ▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▘
│ └───────────────────┘
└── NONE ──────────────────────────────▶
The for loop first calls the iterator’s next
method, and then passes the resulting item (if there
is one) to the loop body. When there are no more items, it exits the loop.
I wanted to follow up my previous post with a small note elaborating on the use of
coroutines for asynchrony and iteration from a more abstract perspective. I realized the point I
made about AsyncIterator
being the product of Iterator
and Future
makes a bit more sense if
you also consider the “base case” - a block of code that is neither asynchronous nor iterative.
It’s also an excuse to draw another fun ASCII diagram, and I’ve got to put that Berkeley Mono license to good use.
…In my previous post, I said that the single best thing the Rust project could do for
users is stabilize AsyncIterator. I specifically meant the interface that already exists in
the standard library, which uses a method called poll_next
. Ideally this would have happened years
ago, but the second best time would be tomorrow.
The main thing holding up the AsyncIterator
stabilization is a commitment by some influential
contributors of the project to pursue an alternative design. This design, which I’ll call the
“async next” design, proposes to use an async method for the interface instead of the poll method of
the “poll next” design implemented today. In my opinion, continuing to pursue this design is a
mistake. I’ve written about this before, but I don’t have the sense my post was
fully received by the Rust project.
Yosh Wuyts, a leading contributor to the async working group, has written his own post about
why the async next design is preferable to poll next. A lot of this is structured as an attempted
refutation of points made by me and others about problems with the async next design. I do not find
the argument in this post compelling, and my position about what the project should do is unchanged.
I’ve written this to attempt to express again, in more detail and more definitively, why I believe
the project should accept the poll next design and stabilize AsyncIterator
now.
I have been devoting a lot of my free time in the past month to thinking about structured concurrency, and a blog post about that is coming soon, but first I want to revisit iterators and generators.
In a previous post, I wrote about one of the hardest problems for generators: self-referential generators. Unlike the Future trait when we were designing async functions, the Iterator trait is already stable, and it does not take a pinned reference to itself. This means an Iterator cannot be self-referential.
…One of the main emphases of my recent posts has been that I believe shipping generators would solve a lot of user problems by making it easy to write imperative iterative code, and especially to make that iterative code interact well with asynchrony and fallibility as well. One thing that frustrates me about the situation is that generators have been nearly ready to ship for years now, but very little visible progress has been made. In particular, the core compiler transform to take a generator and produce a state machine already exists, because it’s exactly how async functions are implemented.
…