Pure Functions & Immutability

Think of a snack vending machine. You press B4, and out drops the same packet of crisps — today, tomorrow, at 3am, no matter who pressed it before you. The machine looks only at the button you push, hands you exactly one thing back, and quietly changes nothing else about the world. A pure function is a vending machine made of code: its output depends only on its inputs, and calling it leaves everything else untouched.

Purity is really two promises rolled into one:

Pure versus impure, side by side

Both functions below add tax to a price. The first is pure — hand it the same price and you always get the same answer. The second secretly reads a global \text{rate} that someone else might change, so the same call can give different answers. Run it, then imagine debugging the second one at 2am.

// PURE: output depends only on the two inputs function withTax(price: number, rate: number): number { return price * (1 + rate); } // IMPURE: reaches out to a global that can change under its feet let taxRate = 0.2; function withTaxImpure(price: number): number { return price * (1 + taxRate); } console.log("pure:", withTax(100, 0.2)); console.log("pure again:", withTax(100, 0.2)); // identical, guaranteed console.log("impure:", withTaxImpure(100)); taxRate = 0.5; // someone changes the world... console.log("impure again:", withTaxImpure(100)); // ...same call, different answer!

The pure function is referentially transparent: \text{withTax}(100, 0.2) is just another way of writing 120. The impure one is not — you cannot know what it returns without knowing the entire history of the program.

Immutability: don't change data, make new data

The sneakiest side effect is mutation — reaching into a list or object you were given and altering it in place. Other parts of the program are still holding that same list, and suddenly it changed under them. The functional cure is immutability: treat data as read-only. Never edit the thing you were handed; build and return a new value instead.

Watch the difference. The impure \text{addImpure} wrecks the caller's array with push; the pure \text{add} spreads the old items into a fresh array and leaves the original alone.

// IMPURE: mutates the array it was given function addImpure(list: number[], x: number): number[] { list.push(x); // reaches into the caller's data! return list; } // PURE: builds a brand-new array, original untouched function add(list: number[], x: number): number[] { return [...list, x]; // spread the old items, then the new one } const original = [1, 2, 3]; const wrecked = addImpure(original, 4); console.log("after impure, original =", original); // [1,2,3,4] — changed! const original2 = [1, 2, 3]; const fresh = add(original2, 4); console.log("after pure, original =", original2); // [1,2,3] — safe console.log("returned new array =", fresh); // [1,2,3,4]

The same habit gives you pure updates for objects ({ ...user, age: 31 }) and pure transforms for whole collections. It feels wasteful to copy — but modern engines are fast, and the peace of mind is enormous: data you didn't return, you didn't change.

A fair worry: a program with zero side effects would be useless — it could never draw a pixel or save a file. Functional programming doesn't ban effects; it corrals them. You keep the vast, testable core of your program pure, and push the messy effects (printing, network, disk, randomness) out to a thin shell at the edges. This is often called "functional core, imperative shell". The pure core is where the thinking lives; the shell just plugs it into the real world. You get the best of both: logic you can trust, and a program that still does things.

Why bother? Three quiet superpowers

The trap is the hidden side effect — a function that looks pure but isn't. The classic offenders:

Beware especially array.sort() and array.reverse(): they sort in place and return the same array, so they look harmless but mutate. Reach for the copy-first version — [...list].sort() — to keep the original safe.