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 just compute that result, it left tracks. A pure function doesn't smell, doesn't taste like anything, leaves no breadcrumbs, footprints or weird DNA material like those you discover when use an UV light in a cheap hotel. It's as if it was never there.
And... Why should I care?
For starters, pure functions are trivial to test. priceWithTax needs no setup, no fake network layer, mock, clock. You call with a bunch of different inputs and validate with a bunch of outputs. These tests run super fast and never flaky, you can even use Swift Testing Parameterised Testing and with a single test validate edge cases and different branch logics, like division by zero and weird inputs, then check the answer:
assert(priceWithTax(82, taxRate: 0.19) == 97.58)To be fair, priceWithTaxLogged isn't much harder to test, at least the returned value from calculation, but the moment something else in the codebase also touches callCount, you will start seeing aliens and fireflies all over the place, tests can start interfering with each other depending on run order and that amazing Macbook you own will say "yes", while CI will say "no", a problem that simply doesn't exist for a function with nothing to interfere with. Oh, and if the tests can go bad, the app itself starts rotting, with a huge risk of bugs in your codebase increasing because of these invisible dependencies and 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.