Skip to content

Rationale · essay 03

Async and IO

rationale.md · 75 lines · 3 min read

The redefinition: async is colorless, and what suspends is the process, not the function. There is no async/await/Future.

IO concurrency requires that something “wait without freezing everything”. The mainstream languages solve this by coloring functions: async fn in Rust/JS/Python marks a function as suspendable, and that mark propagates: whoever calls an async must be async (or await), all the way to the top. The “function coloring” is born: half the language is one color, half another, and the two do not mix.

There is no color. No function is async. Blocking on IO is transparent in the syntax: you write data := -> chan or fs.read(...) as if it were synchronous, and the process suspends there. The scheduler picks up another process while this one waits. The suspension points are explicit and few (io.* operations, channel send/recv, the synchronous <->), but none of them appears as a type mark.

The direct consequence: parallelism is spawn of processes, not composition of Futures. To do N things at the same time, you spawn N processes and receive the results over a channel (ch. 04). There is no Future/await/executor to manage.

The underlying question is: should a function’s signature say how it runs? Makoto’s position is no. A signature says what the function does, never how it is scheduled. “Runs in a suspendable way” is an implementation detail, and an implementation detail should not color the type. That is the distinction that unlocks everything: contract coloring vs implementation coloring. unsafe is contract coloring (it informs you of a real danger you need to know), and it is acceptable. async is implementation coloring (it leaks an execution detail into the type), and it is refused. The operational difference seals the verdict: async is uncontainable (the color rises infinitely up to main); unsafe is containable (any layer can absorb the danger and stop the propagation). A color that does not contain itself is exactly the kind of stain the philosophy (ch. 01) exists to prevent.

This fits into the memory and concurrency models seamlessly: since the process is the unit of execution with a single line (ch. 02/04), “suspend the process” is a natural operation, and it does not need a coroutine state machine exposed to the programmer. The runtime does the work; the language stays colorless.

  • async/await/Future (Rust/JS/Python). Implementation coloring: it infects the type system, forces composition via async blocks, and adds Future as one more first-class type to manage. Real gain over “the process suspends invisibly”: none.
  • Callbacks / promises. Inversion of control, “callback hell”, manual state machines. Worse ergonomics for the same effect.
  • OS threads per request. Too expensive for the grain of concurrency that the BEAM-like wants (millions of processes); and they would reopen shared memory (ch. 04 explains why not).

The classic “what color is your function”: you have a perfectly good synchronous function and discover that you need to call something async inside it. Now it has to be async, and whoever calls it too, and so on, until you rewrite the entire tree or keep two versions of everything. The color that does not contain itself costs real rework, and buys no correctness that the process model does not give for free.

The unit of suspension is the process. You write IO as if it were synchronous; the process stops, the scheduler moves on. Blocking is a fact, not a type mark.

Colorless does not mean “everything implicit”. The <-> (synchronous request-reply) requires an explicit timeout, a mandatory mark, unlike the silent <-. It is a deliberate asymmetry: a deadlock between two processes waiting on each other is structural, and the deadline is the honest way out (ch. 04). And shared-nothing has a copy/message-passing cost; Makoto accepts correctness and native-distribution over raw shared-memory throughput.

Next: 04 · Concurrency