Side-Effects, All the Way Up


There’s a tension at the heart of software that functional programming makes explicit: side-effects are what make programs useful, yet they’re also what make programs hard to reason about.

A pure function is a beautiful thing. Given the same input, it always returns the same output. It doesn’t touch the network, the filesystem, or global state. You can test it in isolation, compose it freely, and reason about it locally. It does exactly what its type signature promises.

But a program made entirely of pure functions is useless. It computes something and then… what? It has to eventually do something with that result. Write it to disk. Send it over the network. Print it to the screen. The moment a program interacts with the outside world, it has side-effects.

This is the core insight of functional programming’s treatment of effects: the goal isn’t to eliminate side-effects. It’s to control them — to make them explicit, visible, and deliberately placed.

Making Effects Explicit

Haskell’s approach is the canonical example. Rather than banning I/O, Haskell tracks it in the type system. A function that performs I/O has that fact encoded in its return type: IO String rather than String. The type signature becomes a contract that tells you exactly what the function might do to the outside world.

This forces effects to the edges of the system. The pure core of your program — the business logic, the data transformations, the decisions — is kept clean. The messy parts, the file reads, the database writes, the HTTP calls, are explicitly marked and deliberately orchestrated.

The practical consequence is that you can look at a function’s type and know whether it can surprise you. A function returning Int can’t write to your database, no matter what’s inside it. A function returning IO Int might do anything.

Algebraic effect systems take this further: instead of a single IO catch-all, you can specify precisely which effects a function might have. It might read from the filesystem but not write to it. It might call out to one API but not another. The type system enforces these constraints.

The insight generalizes beyond Haskell. Even in languages without effect types, the architectural pattern holds: push side-effects to the boundaries, keep your core logic pure, and be deliberate about where and when you interact with the world.

Programs That Change the World

There’s a useful framing here. The reason we write programs at all is to affect the world. The entire point of software is side-effects: storing your data, sending your message, rendering your document, controlling your hardware. A program that computed perfectly and changed nothing would be worthless.

So side-effects aren’t a necessary evil to be minimized. They’re the purpose. The question is only about how to manage them: where they live, what controls them, who can see them.

LLMs and the Same Problem

Now consider a large language model. In isolation, it’s almost perfectly pure in the functional sense: given a prompt (input), it generates a response (output). Same weights, same input, same distribution of outputs. No files touched, no APIs called, no state mutated.

And just like the pure function, this is both its virtue and its limitation.

A language model that can only generate text — that has no access to the outside world — is interesting but constrained. It can’t look up the current date, read your codebase, call an API, or execute a command. It reasons about the world without being able to touch it.

Tools change this entirely. When you give an LLM access to tools — a filesystem, a search engine, a code interpreter, an external API — you’re giving it side-effects. The model is no longer just a text transformer. It’s an agent that can act.

This is where protocols like MCP (Model Context Protocol) come in. MCP is essentially a standard interface for LLM side-effects: a way for models to discover and call tools with well-defined inputs and outputs. It’s the plumbing that turns a language model into an agent capable of doing things in the world.

The Same Lesson, One Level Up

The parallel to functional programming is striking. The FP community spent decades working out how to reason about side-effects in programs. The LLM community is now working out the same thing for agents.

The key questions are identical:

Explicitness. Which effects can this thing produce? A Haskell function’s type tells you. An MCP server’s tool manifest tells you. In both cases, the answer should be discoverable before you invoke the thing, not discovered by observing what it did.

Boundaries. Where do the effects live? In functional architecture, the pure core is kept clean and effects are pushed to the edges. In agent architecture, the same principle applies: the model’s reasoning should be kept separate from its actions. The “thinking” happens in the model; the “doing” happens through tool calls.

Control. Who decides when effects happen? In a pure functional program, effects are orchestrated deliberately — you compose IO actions and choose when to run them. In an agent system, the same question applies: does the model decide autonomously when to write a file and send an email, or does a human approve tool calls before they execute?

Auditing. Can you see what happened? A log of IO actions is the functional equivalent of an agent’s tool call history. Both let you trace what the system did and why.

Explicit effects, controlled and visible, all the way up.

Comments

Came here from LinkedIn or X? Join the conversation below — all discussion lives here.