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.
Here's a small one in F#. A shop runs a happy hour: from 17:00, prices are 20% off; before that, 10% off. finalPrice takes an amount, but it also peeks at the clock to decide the discount, and it writes a line to a log while it's at it. Step through two calls with exactly the same input, and predict the second answer before you look.
Two things are hiding in that function, and they're the two kinds of side effect:
- A hidden input: the clock. The answer depends on it, but the signature
finalPrice (amount: int)never mentions it. Other hidden inputs: random numbers, a global setting someone can change, a file, a web request. - A hidden output: the log. Calling the function changes something outside it. Others: saving to a database, sending an email, changing a global, or sorting the list the caller handed you in place, so their data shifts behind their back.
Hidden inputs make code unpredictable: the same line gives different answers depending on when, where, or after what it runs. That makes bugs hard to reproduce and tests flaky. Hidden outputs make code surprising: you can't tell from the call what else it changed.
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. A test that goes red after five o'clock, with no code change, is a hidden input announcing itself.
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.
Push effects to the edges
Of course, a program that never does anything is useless — at some point you must read the clock, save a record, or show something on screen. The goal isn't to ban side effects, it's to corner them. Keep the effect-having code at the edges of your program (reading input, writing output), and keep the core — your actual logic — pure.
For the pricing function, that means one small move: the hour becomes an argument.
// Pure core: the hour comes in through the front door
let finalPrice hour amount =
let pct = if hour >= 17 then 20 else 10
amount - amount * pct / 100
// Impure edges: read the world once, call the core, write the result
let hour = DateTime.Now.Hour
let price = finalPrice hour 100
log.Add $"priced {price}"
Step through the split version below. When the test runs, predict what the core returns at 17:05 in the evening. Then flip the switch to compare it with the version that reads the clock inside.
Notice what the test gets to skip. It never touches the clock or the log; it talks straight to the core and picks the hour itself. Want to check happy hour? Pass 17. Want to check one minute before? Pass 16. No fake clocks, no waiting until evening, no test that only fails on the night shift.
The edges are still impure, but they're thin and boring: one line reads, one line writes. All the interesting decisions live in the core, where they're easy to test and easy to trust.
A shipping-cost function calls DateTime.Now inside to decide whether weekend rates apply. Its unit test passes Monday to Friday and fails every Saturday. What's the cleanest fix?
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.
Which of these functions is pure?