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:
type _ Effect.t += E : T — Declares a new effect E whose perform yields a value of type T.perform E — Raises the effect, searching the dynamic handler stack for a matching handler.try_with body { effc } — Installs a handler around body. When an effect surfaces, effc is called with the current continuation reified as k.;; Declare an effect
type _ Effect.t += Read : string Effect.t
;; Perform the effect
(def name (perform Read))
;; Handle the effect
(try-with
(fn []
(let [name (perform Read)]
(println "Hello " name)))
(fn [e k]
(match e
Read -> (continue k "World"))))
Continuations in Turmeric are one-shot: calling continue k v consumes k, preventing reuse. This matches Turmeric's ownership model but means no backtracking in v1. (See Logic Programming Guide for cloneable continuations in v2.)
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
type _ Effect.t += Yield : a Effect.t
(try-with
(fn []
(perform (Yield 1))
(perform (Yield 2)))
(fn [e k]
(match e
(Yield v) -> (println v) (continue k))))
Mock I/O operations in tests:
type _ Effect.t += ReadFile : string -> bytes Effect.t
;; Production
(try-with code
(fn [e k]
(match e
(ReadFile path) ->
(continue k (read-file-real path)))))
;; Tests
(try-with code
(fn [e k]
(match e
(ReadFile path) ->
(continue k "mock data"))))
Automatic conflict resolution (see STM Tutorial):
type _ Effect.t += Retry : never Effect.t
(try-with
(fn []
(when (< (read-tvar x) 10)
(perform Retry)))
(fn [e k]
;; Re-run transaction on conflict
(match e
Retry -> (continue k))))
A v2 extension puts effects in the type system via effect rows — sets of effects a function may perform:
val read_file : path -> string @ (ReadFile)
val pure_fn : int -> int @ ()
val map : ('a -> 'b @ 'e) -> 'a list -> 'b list @ 'e
Benefits:
- Polymorphism — map propagates caller's effects through the mapped function.
- Compile-time checking — Verify effect-free code stays pure; catch unhandled effects early.
- Optimization — Pure code (@ ()) can be aggressively refactored.
Status: Planned for v3 of effects design; requires elaborator support and type-system changes.
Effects interact with Turmeric's defer mechanism (§5, §4 of turmeric-plan.md):
defer boundary is the hard case for effects.perform, the captured continuation's environment is cleaned up if it crosses a defer boundary.