Effect Management
Separate the *description* of a side effect from its *execution* — and turn effects into first-class composable values.
So far, our pure functions have one important limitation: they can't do anything interesting to the outside world. They can't read files, call APIs, write to a database, or print to the console. The real world is full of effects, and a real program has to perform them.
The functional approach is not to avoid effects — it's to represent them as values and then run those values in a single, controlled place at the edge of the program. Pure functions stay pure; effects become first-class data.
The core idea: a thunk
The smallest possible "effect" is just a zero-argument function that, when called, produces a value:
That's already the entire trick. An IO<A> is a recipe for
producing an A; nothing happens until you call it. Building one,
passing it around, returning it from a function — all pure.
Running it is the impure act, and it happens at one place: the
top of your program.
Map and chain over IO
Once you have IO<A>, you can build the familiar trio:
Notice what just happened: we composed three steps (read,
transform, print) into one value called program. Building
it did nothing. Only the final program() actually performed any
effect.
This is the structure that more advanced libraries (like Effect or fp-ts) elaborate on enormously: layered effect types that track errors, dependencies, async, and even resource safety in the type system.
Why bother?
Three big reasons:
- Pure tests, real production. You can test functions that
build
IOvalues without ever running them. You only stub the "interpreter" at the very edge. - Effects become composable. Now an effect is just a value.
You can put it in an array, pass it to
map/flatMap, store it in a config, retry it, parallelize it, log it. - The boundary is visible. Effectful types like
IO<A>,Task<A>,Effect<R, E, A>show up in signatures. You can tell from a function's type whether it's pure.
A tiny Console / FileSystem effect
For a slightly more realistic example, here's an effect type with two operations, modeled as a sum type. The "interpreter" runs them against actual implementations.
Same program, two interpreters. The "real" interpreter calls
Date.now(); the "test" interpreter substitutes a fixed value.
Your business logic stays a pure description; the dirty
side-effects are swappable.
This pattern — programs as data, interpreters as functions — is sometimes called Free Monad, Tagless Final, or simply Dependency Inversion done with values.
When is this overkill?
For a small script, an IO<A> = () => A is plenty — you don't
need an algebraic effect type. The point of this chapter isn't
"always use Free Monad", it's:
- Pure functions build descriptions.
- One small impure layer at the edge runs them.
- You can swap that layer for testing, mocking, retries, batching.
That separation is the heart of functional effect management.
A diagram of the boundary
The outer ring is the part that talks to the world. The inner core never does — it only manipulates descriptions of what it would like to happen.
What's the main benefit of representing a side effect as a value (an IO<A> thunk or similar) instead of just running it inline?
It eliminates the need for async/await
It hides the side effect from the rest of the program
It separates describing an effect from running it, so descriptions can be composed, tested, retried, swapped, or interpreted differently without changing the rest of the code
It guarantees the effect will only run once