# ==Algebraic Effects== in ==OCaml 5==, from a ==Haskell== and ==Rust== perspective <p class="doc-sub">// status: seedling</p> I left effect handlers on the “to revisit” list in [[OCaml - learning log]] because they look like one of those features that is easy to describe and hard to place. After working mostly with Haskell's explicit `IO`/monadic style and Rust's `Result` plus async state machines, the useful surprise is that OCaml effects are less about adding another error type and more about making _suspended control flow_ a value the handler can decide what to do with. The central vocabulary is small: - An **effect** is a user-defined operation with a parameter and a result type. - `perform` invokes that operation. - A **handler** surrounds a computation and gives meaning to selected effects. - The handler receives a **delimited continuation** representing the rest of the computation after the `perform`. - `continue` resumes that continuation with a value; `discontinue` resumes it by raising an exception at the suspended point. This is close enough to exception handling to reuse the mental shape, but different enough that “exceptions with better syntax” misses the important part: an exception normally abandons the rest of the computation, while an effect handler may suspend, resume, resume later, resume under a different scheduler, or deliberately never resume. ## The smallest useful example OCaml's effect interface was introduced in 5.0. The pattern syntax for deep handlers was added later, in 5.3, so this exact spelling needs a recent OCaml 5 compiler. The interface is still marked unstable in the standard-library documentation; pin the compiler version when experimenting. ```ocaml open Effect open Effect.Deep type _ Effect.t += Yield : int -> unit Effect.t let producer () = perform (Yield 1); perform (Yield 2); () let run producer = try producer () with | effect (Yield n), k -> Printf.printf "yielded %d\n%!" n; continue k () ``` The `Yield` declaration extends the extensible effect type. Its type says: the performer supplies an `int`, and the suspended operation eventually produces `unit`. `producer` is ordinary direct-style OCaml; it does not mention a callback or a continuation. When `run producer` encounters the first `Yield`, the handler receives `n = 1` and `k`. Calling `continue k ()` returns to the point immediately after the first `perform`, so the same handler catches the second `Yield`. This is a **deep handler**: the handler remains installed while the continuation runs. A shallow handler catches one operation and requires the caller to install the next handler explicitly. A handler can choose a different interpretation just as easily: ```ocaml let run_without_resuming producer = try producer () with | effect (Yield n), _k -> Printf.printf "stopping at %d\n%!" n ``` The continuation is deliberately discarded here. That is a legitimate control decision, but it is not free: any resources held by the suspended computation and any fiber storage remain the programmer's responsibility. ## What the continuation contains It helps to read `k` as “the rest of this particular invocation,” not as a general callback. In the example, it includes the sequence after the `perform`, plus the surrounding computation up to the handler boundary. It is delimited because it cannot jump outside that handler's scope. The continuation has two type parameters. In the manual's scheduler example, a continuation of type `(int, 'a status) continuation` expects an `int` when resumed and eventually produces an `'a status`. That type is why the handler cannot resume a continuation with an arbitrary value; the effect declaration fixes what the suspended operation expects. OCaml continuations are **one-shot**. A captured continuation must be continued or discontinued at most once. Attempting to resume the same continuation twice raises `Continuation_already_resumed`. The language does not provide a static check that it is resumed at least once, so a forgotten continuation can retain a fiber and resources. This one-shot choice is deliberate. Copying a continuation to support multi-shot resumption would have a cost, and duplicating a continuation that owns a socket, file descriptor, or mutable state would be a poor default. The runtime's dynamic check protects the “at most once” side; the handler author still has to satisfy the “eventually once” discipline. ## Effects as a library boundary The compelling part is that the effect's user does not need to know which handler gives it meaning. Consider an exchange operation: ```ocaml type _ Effect.t += Xchg : int -> int Effect.t let first () = perform (Xchg 0) + perform (Xchg 1) let second () = perform (Xchg 21) * perform (Xchg 21) ``` `first` and `second` are direct-style functions. A handler can implement “the next call receives the successor”: ```ocaml let successor_handler f = try f () with | effect (Xchg n), k -> continue k (n + 1) ``` Or it can implement rendezvous between two tasks: suspend the first task with its offered integer and continuation, run another task, and resume both continuations when they meet. The operation's declaration stays the same; the scheduler supplies the meaning. That separation is the effect-handler version of dependency injection, but with control flow included. It can express generators, coroutines, cooperative threads, backtracking, and asynchronous I/O without forcing every user function to be written in continuation-passing style. ### A generator in direct style The same idea explains why a push-style producer can be inverted into a pull-style sequence. The producer calls `yield`; the handler intercepts that call and returns the yielded value plus a continuation that can produce the tail later. ```ocaml type _ Effect.t += Yield_char : char -> unit Effect.t let emit_word () = String.iter (fun c -> perform (Yield_char c)) "OCaml"; () ``` Conceptually, the handler turns each `Yield_char c` into `Seq.Cons (c, suspended_tail)`. The standard manual's `invert` example implements exactly this shape and notes an important consequence: the resulting sequence is ephemeral. If the continuation is one-shot, the consumer cannot freely traverse the same sequence twice. ## Exceptions, effects, and cleanup Effect handlers and exception handlers may be nested, and an unhandled effect is forwarded outward until `Effect.Unhandled` is raised. The runtime does not statically prove that every effect has a handler. OCaml therefore has modular control operations but not effect safety in the sense of languages that track an effect row in every type. `discontinue` matters when a scheduler abandons a suspended task: ```ocaml exception Cancelled (* Given a suspended continuation k: (unit, 'a) continuation: *) (* discontinue k Cancelled *) ``` The exception is raised at the original `perform` point, which lets ordinary cleanup such as `Fun.protect ~finally` run while unwinding the task. Dropping `k` without discontinuing it can leave a task blocked forever, and can keep resources alive. A scheduler that supports cancellation needs an explicit policy for every stored continuation. The effects are synchronous. They cannot safely cross certain callbacks from C into OCaml, and performing an effect from a signal handler, finalizer, memory-profiler callback, or GC alarm is not the same as handling it in normal OCaml code. The low-level power does not remove the runtime's boundaries. ## Haskell: explicit effect values and interpreters Haskell approaches the same design space from a different starting point. In Haskell 2010, `IO a` is an explicit type for an effectful computation, and transformers such as `StateT`, `ExceptT`, and `ReaderT` compose effect representations through monadic operations. A free monad or an extensible-effects library can represent an operation as data and interpret it later. An intentionally small “operation as data” sketch looks like this: ```haskell data Console a = ReadLine (String -> a) | WriteLine String a -- A real interpreter would recurse through a free structure containing -- Console operations, rather than executing IO at the point of construction. ``` The continuation is visible in the constructor (`String -> a`), and an interpreter decides what `ReadLine` means. A monad's bind gives sequencing; an algebraic handler gives each operation a meaning and can, depending on the representation, resume the rest of the program. That resemblance should not become a false equivalence. Haskell's standard `IO` and transformer stacks do not expose OCaml's runtime `Effect.Deep.continuation` API. They represent sequencing in typed values, whereas OCaml's `perform` transfers control to a dynamically enclosing handler. Haskell libraries can implement handlers, and GHC has many extensions beyond Haskell 2010, but “Haskell has monads” is not itself a description of multi-operation, resumable handlers. This difference is visible in the user code. A Haskell program typically advertises the effect carrier in its type (`IO a`, `StateT s m a`, or a custom effect type). An OCaml function can keep a plain return type while relying on a handler installed by its caller; the effect is dynamically scoped rather than statically listed in the function's type. ## Rust: explicit results and compiler-generated state machines Rust's closest everyday tools are `Result<T, E>`, `Option<T>`, iterators, and `Future`. `Result` makes success or failure part of the function signature, and `?` propagates an error by returning early from the enclosing function. This is excellent for non-resumable error handling: ```rust #[derive(Debug)] enum ReadError { Missing, Malformed, } fn read_number(text: &str) -> Result<i64, ReadError> { let trimmed = text.trim(); if trimmed.is_empty() { return Err(ReadError::Missing); } trimmed.parse().map_err(|_| ReadError::Malformed) } ``` An `Err` is data, and a caller can match it, transform it, or return it. But `?` does not capture a continuation that can later be resumed with a replacement value. It performs an early return. Rust's `async`/`await` is closer in control-flow shape. An `async` block creates a `Future`; awaiting it suspends the current future, and the executor polls it again. The compiler-generated state machine stores the data needed across await points and Rust checks its ownership and borrowing rules. That is a powerful suspension mechanism, but it is not a general algebraic-effect handler: the operation is a future/poll protocol, not an arbitrary user-defined `perform` intercepted by a dynamically enclosing handler. ```rust use std::future::Future; async fn fetch_and_add() -> i32 { let left = async { 20 }.await; let right = async { 22 }.await; left + right } fn make_future() -> impl Future<Output = i32> { fetch_and_add() } ``` One can encode generators, schedulers, and effect-like APIs explicitly with enums, callbacks, pinned futures, or macros. The ownership model then makes the lifetime and aliasing story explicit. What Rust does not give me as a stable core primitive is the OCaml pair of `perform` plus a dynamically captured, one-shot continuation handled by pattern matching. ## Why this matters for the things I build The interpreter in [[Monkey language interpreter made in Rust]] separates parsing, evaluation, environments, and errors with explicit data types. A handler-based interpreter could instead use effects for environment lookup, tracing, or suspension, keeping the evaluator in direct style and moving policy to an outer runner. That would be an interesting experiment, but the hidden control flow would need stronger documentation than the current `Result<Value, EvalError>` path. The same thought applies to [[Flappy Bird in Haskell]]. Yampa already packages time-varying behaviour and switching into signal functions; a handler could implement input, scheduling, or resource ownership, but that would not automatically improve the signal model. FRP describes _what changes with time_; effects describe _how an operation transfers control to its interpreter_. They can cooperate without being substitutes. ## A decision rule - Use an exception when the normal continuation should be abandoned and the error is the only result needed. - Use `Result`/`Either` when failure should be explicit in the API and non-resumable. - Use a monadic/free representation when the program should be inspectable, replayable, or interpreted by several backends. - Use an effect handler when a direct-style operation should transfer control to a surrounding policy that may resume, suspend, schedule, or reinterpret it. The last option is especially attractive for concurrency libraries and control inversion. It is also the one that asks for the most discipline: one-shot continuations, cancellation, resource cleanup, dynamic handler scope, and testing for unhandled operations. ## Things that tripped me up - **“Effect” does not mean “effect-safe.”** OCaml's handler machinery does not statically list every effect in a function type. An unhandled operation raises `Effect.Unhandled` at runtime. - **`continue` is not an ordinary callback.** It resumes a delimited computation, and the continuation can only be used once. - **Deep and shallow handlers are not interchangeable.** A deep handler is reinstalled while the continuation runs; a shallow handler catches one operation and needs a new handler for the next one. - **Not resuming is a resource decision.** A dropped continuation can keep fibers and resources alive. Schedulers need `discontinue` or another cancellation path. - **`Result` is not a resumable effect.** Rust's `?` returns early; it does not provide a replacement value for the failed operation. - **Monads and handlers are related ideas, not synonyms.** Monadic bind sequences explicit values; a handler gives semantics to operations and may capture control around them. - **Version matters.** The `effect` pattern syntax in the examples is tied to later OCaml 5 releases than the original 5.0 API, and the standard `Effect` interface is documented as unstable. ## References - [OCaml manual — Effect handlers (5.5)](https://ocaml.org/manual/5.5/effects.html) - [OCaml API — `Effect` module](https://ocaml.org/manual/5.4/api/Effect.html) - [OCaml manual — First-class modules](https://ocaml.org/manual/5.5/firstclassmodules.html) - [Haskell 2010 Language Report — Types and classes](https://www.haskell.org/onlinereport/haskell2010/) - [GHC User's Guide — GADTs and type refinement](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/gadt.html) - [Rust core documentation — `Result`](https://doc.rust-lang.org/core/result/) - [Rust standard library — `Future`](https://doc.rust-lang.org/std/future/trait.Future.html) - [The Rust Programming Language — Traits for async](https://doc.rust-lang.org/book/ch17-05-traits-for-async.html) --- Back to [[Notes/Index|Notes]] · see also [[OCaml - learning log]] · [[GADTs Through a Small Typed Expression Evaluator]] · [[Functional Reactive Programming, Ten Years After Flappy Bird]] · [[Home]].