Effects

Algebraic effects let a function declare a side requirement without knowing how it will be satisfied. The handler decides, and can resume execution from the perform site.

The idea

Exceptions let a function abort up the call stack. Effects go further: the handler can send a value back down to the point where the effect was performed, and execution continues from there. This makes effects useful for dependency injection, logging, non-determinism, and custom control flow.

Declare, perform, handle

There are three steps. First, declare the effect as a named interface:

effect Ask {
  fn prompt(msg) -> str
}

Second, perform the effect inside a function. The function does not know or care how Ask.prompt is implemented:

fn greet() {
  let name = perform Ask.prompt("name?")
  return "Hello, {name}!"
}

Third, wrap the call in a handle block that provides the implementation:

scratch.xs

Resuming with a value

resume(value) inside a handler arm sends the value back to the perform site. Execution of the performing function continues from the next statement after the perform.

scratch.xs

Accumulating effects

The handler body runs every time the effect is performed. You can use this to collect values, count calls, or build up a result.

scratch.xs

The handle form accepts a block as the computation, not just a function call.

Multi-shot resume

A handler arm can call resume more than once. Each call re-enters the captured continuation, running the rest of the computation again with the new value. This is the basis for non-determinism and search.

scratch.xs

Effects vs. exceptions

Exceptions abort and unwind; there is no way to resume. Effects are resumable: the handler gets control, does something, and hands control back to the performing function. Use exceptions when the error is unrecoverable at the call site; use effects when the callee needs information or a capability that the caller should supply.