Dataslope logoDataslope

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:

Code Block
TypeScript 5.7

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:

Code Block
TypeScript 5.7

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:

  1. Pure tests, real production. You can test functions that build IO values without ever running them. You only stub the "interpreter" at the very edge.
  2. 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.
  3. 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.

Code Block
TypeScript 5.7

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.


QuestionSelect one

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

On this page