Confessions of Impurity
A function is mathematically “pure” when the same input results in the same output every time.
x => x * 2 lives a quiet, honest life.
Purity is predictable, easy to reason about and sterile.
When a program touches nothing outside itself, if it never ventures into the real world, it is often useless.
And if adventure is what we seek, then there are a few flavours for our humble function to choose from.
First there is the fog-bound port of Nondeterminism: Things like Math.random(), Date.now(), reading an env var that might change.
CallingDate.now() is the same as asking me what time it is. Ask me three times, and you’ll get a different answer every time. The question was the same, the people talking were the same, but something changed.
“What time is it?” is not a pure question. There is an invisible, confounding factor afoot. This hidden contamination is called a side-effect. Only when a function has no side-effects can it be deemed pure.
We also have the open seas of I/O: Writing a file, making an HTTP request, querying a database, printing to the console, reading a config file. Basically any communication with another system, or entity, that might change.
Mutation is similar, but closer to home — instead of talking to the outside world, you’re changing a variable in your outer scope, or modifying an array that lives outside the function call.
Interestingly, Concurrency belongs on this list too. Kicking off an async task or waiting on a promise that’s already running. The truth is that the world at large can shift from one call to the next.
Exceptions are the sneakiest trapdoor of the lot. They are an exit from the function that doesn’t show up in the return type. The function’s signature says it returns a User, but it might return an error. A lie by omission.
This taxonomy is not exhaustive. You will find no formal claims of completeness here. Rather, we are learning to study the odour of impurity, much like a crime-scene investigator learns to trust their nose long before the lab returns a full forensic accounting.
The villainous nature of the crime in question is one of unmanagement. The problem is undetectable side effects buried inside otherwise normal-looking function calls.
A pure function offers a complete confession — this input, this output, nothing hidden.
But once our function crosses the precipice of purity, its alibi begins to have holes in it.
Effect’s whole trick, and it really is only one trick, is to never call the function.
Never ask for an alibi, and no one can lie to you.
const getUser = (id: string): Effect.Effect<User, DbError, Database> =>
Effect.tryPromise({
try: () => db.query(id),
catch: () => new DbError()
})
Call getUser("123") and — nothing happens. No query fires. What you’re holding is a value: this, when run, will speak to the database; it may fail, and specifically with a DbError; and it requires a Database to be present in its environment before it will even try.
Effect has not repealed the network. It has simply relocated it to a single, supervised location — the runtime, standing at the very edge of your program. Everything else is left honest, inspectable, retriable, composable, and testable with a fake Database swapped in.
You cannot avoid side effects. You were never going to. But you can, it turns out, stop lying about them.