~ / track E / deep theory

Algebraic effects and handlers

Advanced

Algebraic effects are the most exciting development in effect systems since monads. They give you the separation that free monads do (programs describe what they want; handlers decide what happens) without monad plumbing, and they compose linearly — adding an effect doesn't change the type of unrelated code. They're native in Koka, Eff, Unison, and OCaml 5; Scala has them via cats-effect-style libraries and the Effekt language; WebAssembly is shipping them.

The shape

You declare an effect by listing its operations:

effect ask
  fun ask() : int

Code using the effect calls operations like any function:

fun double-asked() : ask int
  ask() * 2

Note the type — ask int means "produces an int, may perform ask." Then a handler discharges the effect by saying what ask should do:

fun main()
  with handler
    fun ask() resume(42)
  double-asked()    // = 84

The handler resumes the computation with 42. That's the magic word.

The continuation

When a program performs ask(), the runtime captures the continuation — "the rest of the program after this call" — and hands it to the handler as resume. The handler can:

  • Call resume(x) once → ordinary effect, like returning x.
  • Call resume(x) multiple times → backtracking, search, generators.
  • Not call resume at all → exceptions, early exit.

This single primitive (delimited continuations as effect handlers) subsumes exceptions, generators, async/await, state, nondeterminism, mock IO, and dependency injection — everything in the monad-transformer zoo.

Compared to monad transformers

Monad transformersAlgebraic effects
Stack order mattersEffects are unordered (a set of capabilities)
Each effect needs lift plumbingCalls look like ordinary functions
Adding an effect changes types of every callerAdding an effect adds one row to the effect row
mtl-style requires typeclass per effectEffects are first-class
n × m instance problemLinear: one operation, one handler

Compared to free monads

Free monads have the same "describe then interpret" split, but the program is a data structure you walk. Algebraic effects skip the intermediate tree — the runtime captures continuations directly. Theoretically equivalent; in practice faster and lower-syntax.

A pseudo-implementation in Clojure

Clojure doesn't have native algebraic effects, but you can fake the shape with dynamic vars as handlers:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

That's the static-resume-once case. True algebraic effects also support multi-shot and zero-shot resumption, which dynamic vars can't do — you'd need full delimited continuations (Clojure has them via cloroutine and similar libraries, with limitations).

Multi-shot example: nondeterminism

A handler that resumes twice implements nondeterministic choice:

effect choose
  fun choose<a>(xs: list<a>) : a

fun pair() : choose (int, int)
  (choose([1,2]), choose([10,20]))

fun all-results()
  with handler
    fun choose(xs)
      flatmap(xs, fn(x) resume(x))   // resume once per option
  pair()
// = [(1,10), (1,20), (2,10), (2,20)]

The body pair() is written as if there were one answer; the handler expands it into the list of all combinations. This is the same trick the list monad plays, but the user code is straight-line.

Why this is the future

  • Linear composition. Effect rows add; they don't nest. Library code stays maximally polymorphic.
  • Effects are decoupled from data structures. The same Console effect can be discharged to real IO, to a mock, to a queue, to a recording — all without changing the program.
  • Async/await falls out for free. Suspension is just "the handler holds the continuation."
  • Mainstream adoption is here. OCaml 5's runtime, .NET's "experimental effect handlers," WebAssembly's stack-switching proposal.

Check yourself

? quiz

What's the key primitive that algebraic-effect handlers expose, that lets them subsume both exceptions and generators?

Exercise

Write down (in any pseudo-syntax) a state effect with operations get : () → s and set : s → (), and a handler that threads the state value through the computation. Show that calling (get, set 1, get) evaluates to something like (s₀, (), 1).

 status: new