Effects System Guide

This guide explains Turmeric's algebraic effects system and when to use it.

Overview

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.

Core Concepts

Effects, Performs, and Handlers

Three core primitives:

;; 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"))

One-Shot Continuations

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.)

Properties

Common Use Cases

Direct-Style Async

Effects enable ergonomic async/await (see Async/Await Guide):

(async
  (await (read-file "data.txt"))
  (println "done"))

Custom Control Flow

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)))

Dependency Injection

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"))

Transactional Retry

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 (Typed Effects)

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.

Syntax

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.

Compiler flags

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

Module-level visibility

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)].

Benefits

Integration with Ownership and Defer

Effects interact with Turmeric's defer mechanism:

See Also