Effect Systems
Motivation
Pure functions are referentially transparent: replacing f(x) with its result preserves program meaning. Effects (mutation, I/O, exceptions, nondeterminism, concurrency) break this property. Effect systems track which effects a computation may perform, enabling reasoning about purity and effect interactions.
The central question: how to integrate effects into typed functional programming while preserving compositionality and type safety.
Monadic Effects
Monads for Effects (Moggi, 1989; Wadler, 1992)
A monad (T, return, bind) encapsulates effectful computation:
return : A -> T A
bind : T A -> (A -> T B) -> T B (written >>= in Haskell)
Monad laws:
- Left identity: return a >>= f = f a
- Right identity: m >>= return = m
- Associativity: (m >>= f) >>= g = m >>= (lambda x. f x >>= g)
Common Effect Monads
| Effect | Monad T A | return a | m >>= f | |---|---|---|---| | Partiality | Maybe A | Just a | case m of Nothing -> Nothing; Just x -> f x | | Exceptions | Either E A | Right a | case m of Left e -> Left e; Right x -> f x | | State | S -> (A, S) | lambda s. (a, s) | lambda s. let (x, s') = m s in f x s' | | Nondeterminism | [A] (list) | [a] | concatMap f m | | I/O | IO A | -- (primitive) | -- (primitive) | | Reader | R -> A | lambda r. a | lambda r. f (m r) r | | Writer | (A, W) | (a, mempty) | let (x, w) = m in let (y, w') = f x in (y, w <> w') | | Continuation | (A -> R) -> R | lambda k. k a | lambda k. m (lambda x. f x k) |
Limitations of Monads
-
Monad composition: Monads do not compose generically. StateT s (Either e) works, but ListT (IO) does not satisfy the monad laws. Monad transformers provide ad-hoc composition but require specific lift operations.
-
Effect ordering: Monad transformer stacks impose a fixed effect ordering. StateT s (Either e) and ExceptT e (State s) have different semantics (does state roll back on exception?).
-
Performance: Deep monad transformer stacks incur overhead. CPS transformation and specialization help but complicate the code.
Algebraic Effects and Handlers
Algebraic Effects (Plotkin-Power, 2003)
An algebraic effect is an interface of operations with specified arities:
effect State S where
get : () -> S
put : S -> ()
effect Exception E where
raise : E -> Empty -- never returns normally
effect Nondeterminism where
choose : () -> Bool
Operations are algebraic in that they satisfy equational theories. For State: get followed by put s is equivalent to put s. The equations constrain handler implementations.
Effect Handlers (Plotkin-Pretnar, 2009)
An effect handler gives semantics to operations by defining how each operation is interpreted:
handle (computation) with
| return x -> ... -- pure result
| get () k -> ...k(s)... -- k is the continuation
| put s' k -> ...k(())... -- resume with ()
The handler receives the operation and a delimited continuation k representing "the rest of the computation." The handler can:
- Resume the continuation (once, multiple times, or never)
- Transform the result
- Thread state through the continuation
Example: State Handler
handler stateHandler (init : S) =
| return x -> lambda s. x
| get () k -> lambda s. (k s) s -- pass current state to continuation
| put s' k -> lambda s. (k ()) s' -- update state
Running: stateHandler 0 (do put 5; x <- get; return (x + 1)) evaluates to 6.
Example: Nondeterminism Handler (Collect All Results)
handler allResults =
| return x -> [x]
| choose () k -> (k true) ++ (k false) -- resume BOTH ways
This calls the continuation twice, collecting all possible results. The ability to invoke k multiple times is what distinguishes handlers from exception handlers.
Effect Polymorphism
Effect polymorphism abstracts over the effects a function may perform:
map : forall a b. forall e. (a -> e b) -> List a -> e List b
Here e is an effect variable. map works with any effectful function without knowing which effects are involved.
Row-Based Effect Systems
Effects are tracked as rows (sets of operation labels):
f : Int -> <State Int, Exception String | e> Int
This says f performs State and Exception effects, plus possibly more effects captured by the effect variable e. The row variable e enables polymorphism.
Row polymorphism (Remy, 1994; adapted for effects by Leijen, 2005):
- Rows are unordered collections of labels
- Row variables capture "the rest of the effects"
- Effect subsumption:
<State | e>is a subtype of<State, IO | e>
Effect Inference
Many effect systems support partial or full inference of effect annotations. The programmer writes effectful code; the compiler infers which effects are performed and checks that all effects are handled.
Languages with Algebraic Effects
Koka (Leijen, 2014-)
Koka is a research language designed around algebraic effects:
- Row-polymorphic effect types
- Handlers as first-class values
- Effect inference (programmer rarely writes effect annotations)
- Perceus reference counting (effect handlers interact with memory management)
effect ask<a>
ctl ask() : a
fun greeting() : ask<string> string
val name = ask()
"Hello, " ++ name
fun main()
with handler
ctl ask() resume("Alice")
greeting().println
Eff (Bauer-Pretnar, 2012)
Eff pioneered the handler-based approach:
- First-class effects and handlers
- Type-and-effect system
- Clean theoretical semantics
Frank (Lindley-McBride-McLaughlin, 2017)
Frank integrates effects directly into the type system via multi-handlers:
- Handlers are just functions that pattern-match on effectful computations
- No syntactic distinction between handling and calling
- "Do be do be do" -- effectful and pure computations interleave naturally
OCaml 5
OCaml 5 added algebraic effects (via Effect.Deep and Effect.Shallow modules) using one-shot continuations. Effects are used for:
- Cooperative concurrency (fibers)
- Async I/O
- Custom schedulers
Delimited Continuations
Effect handlers are closely related to delimited continuations (Felleisen, 1988; Danvy-Filinski, 1990):
- shift/reset:
reset (... shift k. M ...)captures the continuation up to the nearestresetand binds it to k in M. - prompt/control: Similar, but
controldoes not include the delimiter in the captured continuation.
An effect handler handle ... with {op k -> ...} is essentially:
handleestablishes a delimiter (likereset)- Performing an operation captures the continuation to the handler (like
shift) - The handler body decides what to do with the continuation
Theorem (Forster-Kammar-Lindley-Pretnar, 2019): Algebraic effects with handlers and delimited continuations (shift/reset) are macro-expressible in terms of each other.
One-Shot vs Multi-Shot Continuations
- One-shot: Continuation invoked at most once. More efficient (no copying). Sufficient for exceptions, state, coroutines.
- Multi-shot: Continuation can be invoked multiple times. Required for nondeterminism, backtracking. Requires copying the continuation (expensive).
OCaml 5 supports only one-shot continuations for performance. Multicore OCaml uses them for fiber-based concurrency.
Free Monads
Free monads provide an alternative encoding of algebraic effects in languages without native handler support:
data Free f a = Pure a | Free (f (Free f a))
-- For State:
data StateF s next = Get (s -> next) | Put s next
type StateFree s = Free (StateF s)
A free monad builds a syntax tree of operations, which is then interpreted by a handler (a natural transformation):
interpret :: (f (Free f a) -> Free g a) -> Free f a -> Free g a
Freer monads (Kiselyov-Ishii, 2015) improve on free monads:
data Freer f a where
Pure :: a -> Freer f a
Impure :: f x -> (x -> Freer f a) -> Freer f a
This CPS-style representation avoids the quadratic bind problem of naive free monads and does not require Functor instances.
Performance
Free/freer monads have overhead from building and interpreting syntax trees. Techniques for optimization:
- Church-encoded free monads: CPS transform eliminates left-nested binds
- Fused effects (Haskell library): Uses
ReaderT IORefat the bottom, interpreting most effects efficiently - Polysemy / Effectful: Modern Haskell libraries using various strategies (type-class dispatch,
IO-based)
Comparison of Approaches
| Approach | Composability | Performance | Type Safety | Flexibility | |---|---|---|---|---| | Monad transformers | Manual (lift) | Good (specialized) | Strong | Fixed stack order | | Free monads | Good | Moderate (interpretation overhead) | Strong | Flexible | | Algebraic effects | Excellent | Good (compiled) | Strong | Handler-based | | Effect type classes (mtl) | Good (no lift) | Good (inlining) | Strong | Constrained |
Theoretical Foundations
Lawvere Theories
Algebraic effects correspond to Lawvere theories: a Lawvere theory is a category with finite products where objects are natural numbers. Operations of arity n are morphisms n -> 1. Models are product-preserving functors to Set.
Scoping and Tunneling
Not all effects are algebraic. Scoped effects (Wu-Schrijvers-Hinze, 2014) handle effects with scope (like catch for exceptions, which scopes over a block). Effect tunneling (Zhang-Myers, 2019) ensures that effects propagate correctly through handler boundaries.
Typed Algebraic Effects
Bauer and Pretnar (2014) developed a full type-and-effect system for handlers:
- Effect types are rows of operation signatures
- Handler types:
A ! E => B ! E'(transform computation of type A with effects E into result of type B with effects E') - Effect subtyping via row inclusion
Categorical Semantics
- Effects as monads (Moggi): The Kleisli category of a monad models effectful computation
- Effects as adjunctions: Every monad arises from an adjunction; different adjunctions give different computational interpretations (state, continuations, etc.)
- Handlers as algebra homomorphisms: A handler for effect E is an E-algebra (Plotkin-Pretnar)
Current Research Directions
- Effect tunneling and lexical effects: Ensuring effects respect lexical scope (not dynamic scope)
- Higher-order effects: Effects whose operations take effectful computations as arguments (e.g.,
catch : (() -> a) -> (exn -> a) -> a) - Effect inference with principal types: Can effect types be inferred completely?
- Efficient compilation: Compiling handlers to native code without continuation overhead (evidence-passing, selective CPS)
- Integration with dependent types: Idris 2 has both effects and QTT; interaction is an active area