Coroutines, asynchronous and iterative

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.

By the “base case,” I mean just a normal block. This isn’t really a “coroutine” in the way the others are, of course, because its evaluated immediately in Rust, but you could imagine a language with lazy blocks that aren’t eagerly evaluated. I don’t want to get too in-the-weeds about laziness, I already know someone’s going to make some shallow comment on Reddit about Haskell and monads and how Rust’s design is fatally flawed, an opinion they are certainly free to hold. But if you ignore the distinction between lazy and eager semantics, you can imagine an ordinary block as also a kind of coroutine - one which is always ready (not asynchronous) and only evaluates once (not iterative).

What you get then is a diagram with two axes: asynchrony and iteration, and you can see how starting from the base case, you can move toward AsyncIterator by either path:

                         A S Y N C H R O N O U S
                        ──────────────────────── ▶

             ╔═══════════════╗              ╔═══════════════╗   
             ║               ║░░            ║               ║░░
             ║   BASE CASE   ║░░            ║    FUTURE     ║░░
             ║               ║───────────── ▶               ║░░
         │   ║      { }      ║░░            ║   async { }   ║░░  │  
       I │   ║               ║░░            ║               ║░░  │ I
       T │   ╚═══════════════╝░░            ╚═══════════════╝░░  │ T
       E │     ░░░░░░│░░░░░░░░░░              ░░░░░░│░░░░░░░░░░  │ E
       R │           │                              │            │ R
       A │           │                              │            │ A
       T │           │                              │            │ T
       I │           │                              │            │ I
       V │   ╔═══════▼═══════╗              ╔═══════▼═══════╗    │ V
       E │   ║               ║░░            ║               ║░░  │ E
         ▼   ║   ITERATOR    ║░░            ║ ASYNCITERATOR ║░░  ▼  
             ║               ║───────────── ▶               ║░░
             ║    gen { }    ║░░            ║ async gen { } ║░░
             ║               ║░░            ║               ║░░
             ╚═══════════════╝░░            ╚═══════════════╝░░
               ░░░░░░░░░░░░░░░░░              ░░░░░░░░░░░░░░░░░

                        ──────────────────────── ▶
                         A S Y N C H R O N O U S

This dynamic is also born out by adding the base case to the typing table from my previous post:

                  │   YIELDS            │   RETURNS       │   RESUMES
    ──────────────┼─────────────────────┼─────────────────┼─────────────────
                  │                     │                 │      
        BASE CASE │   !                 │   Self::Output  │   ()
                  │                     │                 │      
           FUTURE │   ()                │   Self::Output  │   &mut Context
                  │                     │                 │      
         ITERATOR │   Self::Item        │   ()            │   ()   
                  │                     │                 │      
    ASYNCITERATOR │   Poll<Self::Item>  │   ()            │   &mut Context
                  │                     │                 │      

You can see how the baser case and Future have the same return type, while the base case and Iterator have the same resume type; AsyncIterator combines both Iterator and Future. And the yield type actually reveals of the same progression on further consideration: the base case is !, an uninhabited type (a normal block never yields), whereas Future yields the unit type to represent Pending, and Iterator yields the item type: the AsyncIterator then yields the sum of Future and Iterator: a type that is either the item type or pending.