This guide explains Turmeric's algebraic effects system and when to use it.
Turmeric implements algebraic effect handlers inspired by OCaml 5, enabling ergonomic asynchronous programming, custom control flow, and composable abstractions. Effects allow you to perform operations that handlers can intercept and re-route dynamically.
Three core primitives:
(defeffect Name [param :type ...] :return-type) -- Declares a new effect Name.(perform (Name arg ...)) -- Raises the effect, searching the dynamic handler stack for a matching handler.(handle expr (Name [p ...] k) body ...) -- Installs a handler. When the effect fires, k is the captured continuation; call (resume k value) to continue.;; Declare an effect
(defeffect Read [] :str)
;; Perform the effect
(def name (perform (Read)))
;; Handle the effect
(handle
(let [name (perform (Read))]
(println (str "Hello " name)))
(Read [] k) (resume k "World"))
Continuations in Turmeric are one-shot: calling (resume k v) consumes k, preventing reuse. This matches Turmeric's ownership model. (See Logic Programming Guide for cloneable continuations.)
perform is handled is checked dynamically. Unhandled effects raise an exception at runtime.Effects enable ergonomic async/await (see Async/Await Guide):
(async
(await (read-file "data.txt"))
(println "done"))
Implement generators, early returns, or custom exception handling:
;; Generator: yield values one at a time
(defeffect Yield [v :int] :nil)
(handle
(do
(perform (Yield 1))
(perform (Yield 2)))
(Yield [v] k) (do (println v) (resume k nil)))
Mock I/O operations in tests:
(defeffect ReadFile [path :cstr] :str)
;; Production
(handle code
(ReadFile [path] k) (resume k (read-file-real path)))
;; Tests
(handle code
(ReadFile [path] k) (resume k "mock data"))
Automatic conflict resolution (see STM Tutorial):
(defeffect Retry [] :nil)
(handle
(fn []
(when (< (read-tvar x) 10)
(perform (Retry))))
;; Re-run transaction on conflict
(Retry [] k) (resume k nil))
Effect rows track which effects a function may perform as part of its type. The compiler infers rows automatically; you can also annotate them explicitly.
An effect row appears between the parameter list and the return type:
;; Annotated: may perform the Write effect
(defn log-msg [msg :cstr] #{Write} :nil
(perform (Write msg)))
;; Pure: performs no effects
(defn add [a :int b :int] #{} :int
(+ a b))
;; Row-polymorphic: propagates the row of the function argument
(defn run-twice [f :(fn [] #{e} :int)] #{e} :int
(+ (f) (f)))
The row #{e} is a row variable: run-twice performs whatever effects f performs, no more.
| Flag | Effect |
|---|---|
--dump-effects |
Print each top-level defn's inferred effect row after checking |
--lint-effects |
Warn on unannotated defns whose inferred row is non-empty |
--strict-effects |
Under --strict-effects, unannotated functions that perform effects get a warning; callers propagate the inferred row |
Effects can be declared ^private to prevent leakage outside their defining module:
(defeffect ^private InternalLog [msg :cstr] :nil)
A ^private effect cannot be performed or handled outside the module that declares it.
Cross-module effect rows are automatically filtered: if a callee internally performs a private
effect, the caller's inferred row does not include it.
To export an effect explicitly:
(defmodule MyLib
(export (effect Write) (effect Read))
...)
Other modules import it with :refer [(effect Write)].
TUR-E0009).--dump-effects shows the full effect signature of every function.Effects interact with Turmeric's defer mechanism:
defer cleanup runs correctly even when perform is inside the same do block (see Custom Effects Tutorial §8).defer boundary is handled: the continuation's environment is cleaned up if it crosses a defer boundary.