Pure Functions: The Same Answer, Every Time
A function that never surprises you
Here's a function that calculates the price of a product including tax:
func priceWithTax(_ subtotal: Decimal, taxRate: Decimal) -> Decimal {
subtotal * (1 + taxRate)
}Call it three times with the same arguments:
priceWithTax(82, taxRate: 0.19) // 97.58
priceWithTax(82, taxRate: 0.19) // 97.58
priceWithTax(82, taxRate: 0.19) // 97.58Same answer, every time. That's not an accident of this particular example — it's true no matter when you call it, how many times you've called it before, what else is running in the program, what today's date happens to be, whether it's raining, whether you ate your broccoli as your mama told you, or whether your device is set to Korean and you sing Backstreet Boys in the shower (not judging). priceWithTax only looks at the two values you hand it, and it only communicates back through its return value. It doesn't read a clock, a network connection, a global variable, your CO2 emissions, or anything else sitting outside its own parameter list. That's what "pure" means: same inputs, same output, and no observable interaction with the world other than taking arguments in and handing a result back.
What impure looks like
Contrast that with a version that logs every call:
var callCount = 0
func priceWithTaxLogged(_ subtotal: Decimal, taxRate: Decimal) -> Decimal {
callCount += 1
print("call #\(callCount)")
return subtotal * (1 + taxRate)
}Run this three times and the return value is still 97.58 every time — but that's no longer the whole story. Each call also mutates callCount and prints to the console. Those are side effects: changes to the world that happen outside the return value, and that anyone else touching callCount or watching stdout can observe. The output is honest proof of the same input producing the same result, but the function did more than compute that result — it left tracks. A pure function leaves none.
Why this is worth caring about
Pure functions are easy to test. priceWithTax needs no setup, no fake network layer, no mock clock — you call it and check the answer:
assert(priceWithTax(82, taxRate: 0.19) == 97.58)priceWithTaxLogged isn't much harder to test in isolation, at least the returned value from
calculation, but the moment something else in the codebase also touches callCount, tests can start interfering with each other depending on run order — a problem that simply doesn't exist for a function with nothing to interfere with. And not only the tests start interfering with each other, but the risk of bugs in your codebase increases because of these invisible dependencies and above all, invisible behaviour.
The Chain Effect (pun intended)
Pure functions are also safe to combine. Feed the output of one straight into another and there's no hidden coordination to worry about — no shared state that has to be reset in between, no ordering dependency beyond the data flowing from one call to the next:
func applyDiscount(_ price: Decimal, percentOff: Decimal) -> Decimal {
price * (1 - percentOff)
}
applyDiscount(priceWithTax(82, taxRate: 0.19), percentOff: 0.1) // 87.822And because the same input always produces the same output, a pure function is in principle cacheable — memoizable. Look up priceWithTax(82, taxRate: 0.19) once, and the answer is good forever; nothing about the world changes what it should have returned.
When combining/composing two pure functions, the result is a pure function. Make one of them impure and the whole thing is contaminated, and everything that composes with that and so on. That means one tiny impurity in one of them, and something explodes somewhere else in the system: best case scenario, a test goes flaky; more realistically, your Customer Support colleague has to say "I'm sorry" three hundred times to an angry customer. When you lose track of purity, it's easy to create implicit co-dependencies and eventually a tangled mess of code that has more holes than Swiss cheese. Not everything can be pure, of course — at some point you have to read data from somewhere outside of your Wonderland Purity Bubble (™, patent pending), but in the future we will explore techniques such as "Functional Core, Imperative Shell" that let you isolate the impure parts of your codebase and keep most of it pure, testable, and composable, with no surprises.
Purity can break in two directions — a function can quietly do something to the world beyond its return value, or quietly let something from the world leak in beyond its parameters — and each of those is its own topic still to come.