Effect handlers and monads both manage side effects in functional programs, but they solve the composition problem differently. Monads require explicit lifting and threading through every function in the call stack. Effect handlers let any function in the call stack intercept and handle an effect without the intermediate functions knowing about it. When you have multiple interacting effects or need resumable control flow, effect handlers are dramatically simpler to compose.
Pithy Cyborg | AI FAQs – The Details
Question: When should you use effect handlers (like in Koka or Effekt) instead of monads for algebraic effects in functional programming?
Asked by: Claude Sonnet 4.6
Answered by: Mike D (MrComputerScience)
From Pithy Cyborg | AI News Made Simple
And Pithy Security | Cybersecurity News
Why Monads Struggle With Multiple Composed Effects
Monads are an elegant solution for managing a single effect. The IO monad handles input and output. The Maybe monad handles failure. The State monad handles mutable state. Each is clean and principled in isolation.
The problem surfaces when a real program needs multiple effects simultaneously. A function that can fail, reads from state, and performs IO needs to compose all three monads. This is done with monad transformers: MaybeT (StateT (IO a)) stacks the transformers in a specific order. Each layer adds boilerplate. Functions operating in the inner monad must lift their operations through the outer layers with explicit lift calls. The ordering of transformer layers is not commutative: StateT (MaybeT IO) and MaybeT (StateT IO) have subtly different semantics that determine whether state is preserved or discarded on failure, and getting this wrong produces bugs that are hard to reason about.
In Haskell, a codebase with four or five effects in play accumulates significant transformer stack complexity. Functions deep in the call hierarchy must know the full monad stack to lift through it correctly, even if they only care about one effect. This is the infamous “n-squared problem” of monad transformers: n effects require on the order of n² lifting functions to compose cleanly.
The MTL (Monad Transformer Library) approach using type classes like MonadState, MonadError, and MonadIO alleviates the mechanical lifting but does not eliminate the fundamental composition complexity. Every new effect requires a new type class, new instances for every transformer combination, and careful reasoning about transformer ordering.
How Algebraic Effect Handlers Solve Composition Differently
Algebraic effects separate the declaration of an effect from its implementation. An effect is declared as a set of operations with types, similar to an interface. A function that uses the effect states this in its type signature. Any caller up the stack can install a handler that provides an implementation for those operations. Functions between the effect user and the handler do not need to know about the effect at all.
This is the key difference from monads. In a monad transformer stack, every intermediate function must participate in threading the effect through. With effect handlers, intermediate functions are transparent to effects they do not handle. A deeply nested function can perform a logging effect and the handler for logging can sit at the top level of the program without any of the intermediate call stack needing to import, lift, or acknowledge the logging effect.
In Koka, designed by Daan Leijen at Microsoft Research, effects are tracked in function types and composed automatically. A function that performs state and exception effects has a type like (s : State<Int>, e : Exn) => Int. The compiler verifies effect safety statically and generates efficient code using evidence passing rather than heap allocation, achieving performance comparable to direct style code without a runtime effect interpreter.
Effekt, a research language from Tübingen, pushes further by supporting bidirectional effects where handlers can resume the computation that raised the effect zero, one, or multiple times. This enables user-space coroutines, backtracking search, probabilistic programming, and cooperative multitasking as library-level constructs rather than language primitives. Local LLM vs API for Python development hits an analogous architectural choice: whether to bake async IO handling into every layer of your call stack (monads), or route it through a framework that handles the effect transparently (async runtimes like asyncio and Trio, which are operationally similar to effect handlers for the IO effect).
When to Choose Each Approach in Practice
The decision is not purely theoretical. Different languages and codebases make different tradeoffs practical.
Use monads when you have one or two effects and you are working in Haskell or another language with mature monad tooling. A single IO monad or an IO + Either combination is clean, well-understood by anyone who knows Haskell, and has no performance overhead. The Haskell ecosystem is built around monads and fighting that convention in a Haskell codebase adds friction without benefit.
Use monad transformers when you have three or four effects and the MTL type classes are already in your dependency tree. Accept the boilerplate, be careful about transformer ordering, and document the semantics of your stack explicitly. ReaderT IO is a widely used pattern in production Haskell that threads configuration and IO cleanly without excessive complexity.
Use effect handlers when you have five or more interacting effects, need resumable exceptions or coroutine-style control flow, or are designing a new language or library where composition ergonomics matter more than ecosystem compatibility. Koka, Effekt, OCaml 5’s effect system (stabilized in 2024), and the Unison language all support effects natively. OCaml 5’s effects are particularly practical because OCaml has a large existing ecosystem and the effect system is a production-grade addition rather than a research prototype.
For Haskell specifically, the Polysemy and Effectful libraries implement algebraic effects as embedded DSLs, giving you effect handler semantics within the Haskell type system. Effectful in particular has production-quality performance and is used in several large Haskell codebases as a replacement for the MTL transformer stack.
What This Means For You
- Default to monads or MTL in Haskell for codebases with up to three or four effects. The ecosystem familiarity and tooling maturity outweigh the ergonomic advantages of effect handlers in most cases.
- Reach for Effectful or Polysemy when your Haskell transformer stack has grown past four effects or when transformer ordering bugs are causing recurring issues. The algebraic effect model eliminates the ordering problem by construction.
- Use OCaml 5’s native effect system for new OCaml projects that need coroutines, cooperative multitasking, or complex control flow. It is the most practical production-grade effect handler system available in a mainstream language as of 2026.
- Study Koka for the conceptual model even if you never deploy it. Leijen’s papers on Koka contain the clearest explanations of how effect typing works and why it composes better than monads, and the concepts transfer to any language.
- Recognize effect handlers in disguise in languages you already use. Python’s async/await, JavaScript’s generators, and Ruby’s Fiber all implement fragments of algebraic effect handling without the type safety, and understanding the correspondence makes those mechanisms more predictable.
Pithy Cyborg | AI News Made Simple
Subscribe (Free): https://pithycyborg.substack.com/subscribe
Read archives (Free): https://pithycyborg.substack.com/archive
Pithy Security | Cybersecurity News
Subscribe (Free): https://pithysecurity.substack.com/subscribe
Read archives (Free): https://pithysecurity.substack.com/archive
