Summary

Matt Might’s tutorial on continuation-passing style (CPS) in JavaScript, derived from the constraint “no procedure may return to its caller.” CPS transforms functions to take an extra callback (the “continuation”) instead of returning values. The article covers CPS for Ajax/node.js non-blocking I/O, distributed computation, exception handling, and as a compiler intermediate representation.

Matt Might 的 CPS 教學:所有函數改為接受「下一步要做什麼」的回呼而非直接返回值。涵蓋 Ajax 非阻塞 I/O、Node.js 伺服器、例外處理反糖化,以及 CPS 作為編譯器中間表示的應用。

Key Points

  • Core transformation: function id(x) { return x }function id(x,cc) { cc(x) } — every function takes a continuation callback as last argument
  • Why it matters for Ajax: blocking I/O freezes the browser; CPS makes async natural — fetch(url, callback) inverts control flow without callbacks being an afterthought
  • Exceptions in CPS: add a second continuation thro alongside ret; try/catch desugars to wrapping calls with the error continuation
  • Distributed computation: CPS makes it trivial to swap local fact for a remote async version — the call sites don’t change
  • Compiler IR: CPS has been the standard intermediate representation for functional language compilers for 30+ years; function call becomes a single jump instruction
  • call/cc implementation: trivial in CPS — callcc(f,cc) = f(lambda(x,k) { cc(x) }, cc) — call/cc just captures the current continuation as a first-class value

Insights

CPS is the underlying model that JavaScript’s async/await was designed to hide. Understanding CPS explains why async is “viral” (each function that awaits must itself be async) — in CPS, every function that takes a continuation must pass continuations through. The async/await sugar removes the explicit continuation threading but doesn’t remove the underlying requirement.

The node.js non-blocking IO design was explicitly CPS-based: all I/O functions take callbacks, which are continuations. This was a deliberate choice to avoid blocking the single event loop thread.

The compiler IR connection explains why async/await is expressible in terms of state machines (as TypeScript and Babel compile it): CPS is a normal form where control flow is explicit, making it straightforward to turn into a state machine where each await becomes a state transition.

Connections

Raw Excerpt

No procedure is allowed to return to its caller—ever. Procedures can take a callback to invoke upon their return value. A continuation is a first-class return point.