Chapters

Understanding Algebraic Effects

Separate what a computation requests from how that request is handled.

9.1 perform requests an operation

When a function needs to request an operation, Tauto can express it with algebraic effects. The caller uses perform to make the request; an enclosing handle gives it meaning.

ch09-ask.taut
effect ask(n: i64): i64 resume ω

let result := handle {
  perform ask(5) + 1
} with {
  ask(n), k => k(n) + k(n + 1)
  return v => v
}

perform show("result", result)

// Resume one continuation twice and add the two results.
Execution result
result = 13
result = 13

ask(5) is not an ordinary function call. The handler receives the rest of the computation after the operation as k.

9.2 Follow the continuation k

In perform ask(5) + 1, the remaining computation adds 1 to the answer. Therefore k(5) returns 6, and k(6) returns 7. The handler adds them to produce 13.

resume ω permits the continuation to be resumed any number of times (the next chapter explains this in more detail). Ownership checking and resumption quantities work together to prevent duplication of a continuation from consuming a resource twice.

9.3 Express failure with a handler

ch09-fail.taut
effect fail(why: Text): i64

fn safe_div(a: i64, b: i64): i64 {
  if b == 0 {
    perform fail("division by zero")
  } else {
    div(a, b)
  }
}

let answer := handle { safe_div(84, 0) } with {
  fail(why), k => 0 - 1
  return value => value
}
perform show("answer", answer)

// Handle division failure by discarding the remaining computation.
Execution result
・ safe_div

answer = -1
answer = -1

This handler does not call k. It returns -1 without returning to the remaining computation, producing an exception-like early exit.

The standard library also has a fail, declared as fail<E>: (e: E) -> Never resume 0. Its result is Never and its resumption quantity is 0: it does not resume. The fail declared in this example is a separate, local operation. For an effect that never resumes, do not write k at all.

9.4 Effect rows

An annotation such as ! {out_of_bounds} after a function's result type describes the effects that remain for its caller to handle. ! {} means that no effects remain. Effect rows are checked when functions are composed.

Exercise 9.1: What value would the program produce if the handler became ask(n), k => k(n) * k(n + 1)?