Think about a vending machine. You press B4, you get the same bag of pretzels every single time. It doesn't care what day it is, it doesn't phone a friend, and it doesn't quietly rearrange the other snacks while you're not looking. A pure function is exactly that polite: give it the same input and it always hands back the same output, and it touches absolutely nothing else.
The problem
Most functions we write are sneakier than a vending machine. They read the current time, fetch data over the network, scribble to a database, or quietly change a global variable sitting somewhere else in the program. These hidden behaviors are called side effects, and a function that has them is called impure.
The trouble is that side effects make code unpredictable. Call the same impure function twice and you might get two different answers, because the world changed underneath it. That makes bugs hard to reproduce, tests hard to write, and the whole program hard to reason about — you can never be sure what a line of code really does just by reading it.
- Impure fReaches out to the clock, network, and a global — so its answer changes from call to call.
- Hidden inputState the function reads or changes that isn't passed in as an argument.
Here's an impure function in F#. It looks innocent, but its answer changes every time you run it:
// Impure: depends on the clock, so the output is never the same twice
let greeting () =
let now = System.DateTime.Now
sprintf "The time is %s" (now.ToString("HH:mm:ss"))
You can't write a reliable test for this — what would you even check it against? The answer is a moving target.
Side effects aren't evil — saving a file or sending an email is the whole point of many programs. The danger is hidden side effects buried inside functions that look like simple calculations. Surprises in code are where bugs love to hide.
How it works
A pure function follows two simple house rules. First, its output depends only on its inputs — same arguments in, same result out, forever. Second, it causes no side effects — it doesn't reach out to change anything in the outside world. Everything it needs comes in through the front door as arguments, and everything it produces goes out the front door as a return value.
Here's the purest little function imaginable:
// Pure: output depends only on a and b, and nothing else is touched
let add a b = a + b
add 2 3 // always 5
add 2 3 // still 5, every single time
No clock, no database, no network. Just an honest answer you can count on.
- Pure functionOutput depends only on the input — no clock, no globals, no side effects.
The animation above shows the idea in motion: the same input flows into the function box and the same output always comes out the other side. Notice there are no arrows reaching out to a clock, a database, or anything else — the box is sealed off from the outside world. That self-contained quality is the whole secret.
Why purity is wonderful
Once a function is pure, lovely things become true almost for free:
- Trivially testable — feed in an input, check the output. No fake clocks, no test databases, no elaborate setup.
- Cacheable — since the same input always gives the same output, you can remember (or memoize) past results and skip the work next time.
- Safe in parallel — pure functions don't share or change hidden state, so you can run thousands of them at once without them stepping on each other.
- Easy to reason about — what you read is exactly what happens. No spooky action somewhere else.
Purity is a close cousin of idempotency. Both are about predictable repetition: calling a pure function twice with the same input is harmless because nothing changes outside it. Keeping your data unchangeable — see immutability — makes purity even easier to achieve.
Push effects to the edges
Of course, a program that never does anything is useless — at some point you must read a file, save a record, or show something on screen. The goal isn't to ban side effects, it's to corner them. Keep the messy, effect-having code at the edges of your program (reading input, writing output), and keep the core — your actual logic — pure.
In practice that means doing the impure work first, then handing plain values to a pure function:
// Impure edge: grab the current time once, out here
let now = System.DateTime.Now
// Pure core: takes the time as an argument, easy to test
let greetingFor (time: System.DateTime) =
sprintf "The time is %s" (time.ToString("HH:mm:ss"))
greetingFor now
Now the logic lives in greetingFor, which is perfectly pure and testable — you can pass it any time you like — while the single impure line stays out at the edge where it's easy to see.