Programming foundations Mutability and pure functions

Mutability and pure functions

Why changing data in place causes surprises, and how to keep decisions separate from effects.

  • 14 min read
  • 6 chapters
  • Lesson 2 of 4

What you will learn

  1. 01Changing things in place
  2. 02What makes a function pure
  3. 03Why purity pays off
  4. 04Updating without mutating
  5. 05Functional core, imperative shell
  6. 06A checklist to carry forward
01

Changing things in place

Data is mutable if it can be changed after it is created, and immutable if every change produces a new value instead. Mutation is natural and fast, but it has a hidden cost: it affects everyone who holds a reference to the same data.

const a = [1, 2, 3];
const b = a;
b.push(4);
// a is now [1, 2, 3, 4] as well

b was supposed to be a copy, but both names point at one list. This is called aliasing, and it produces bugs that appear far from their cause, because the code that changed the data is not the code that later reads it.

02

What makes a function pure

A pure function has two properties. For the same inputs it always returns the same output, and it has no side effects, meaning it changes nothing outside itself. fee(minutes) from the last lesson is pure.

These make a function impure:

  • Reading the clock, a random number, a file, or a global variable.
  • Writing to a database, the screen, a log, or the network.
  • Changing an argument that the caller still holds.
// impure: depends on the clock and mutates shared data
function leave(id: string) {
  const t = lot.get(id)!;
  lot.delete(id);
  return fee((Date.now() - t.enteredAt) / 60000);
}
03

Why purity pays off

Pure functions are cheap to live with.

  • Testing: no setup, no fakes. Pass values in, check the value out.
  • Reasoning: you understand the function by reading only the function.
  • Caching: since the answer never changes for given inputs, you can safely remember it.
  • Concurrency: with nothing shared and nothing written, two threads cannot interfere.

The tradeoff is real. Programs exist to have effects, since a parking lot that cannot save tickets is useless. Purity is therefore not a goal for every line. It is something you protect where you can and push effects to the edges.

04

Updating without mutating

Instead of editing a value, create a changed copy.

const moved = { ...ticket, plate: "KA01AB1234" };
const longer = [...tickets, newTicket];
const cleaned = tickets.filter((t) => t.id !== id);

The original is untouched, so anyone holding it still sees a consistent value. Copying sounds slow, but you copy only the outer shell, and many libraries share the unchanged parts between versions. Mutation is still fine where nobody else can see it: a local variable inside one function, or a hot loop measured to be a bottleneck.

05

Functional core, imperative shell

The most useful design idea here is to split a program in two. A pure core makes decisions from plain values. A thin shell around it does the messy work: read inputs, call the core, then perform the effects it asks for.

// core: pure, easy to test
function settle(t: Ticket, now: number): { fee: number } {
  return { fee: fee((now - t.enteredAt) / 60000) };
}

// shell: effects live here
function leave(id: string) {
  const t = lot.get(id);
  if (!t) throw new Error("unknown ticket");
  const result = settle(t, Date.now());
  lot.delete(id);
  return result.fee;
}
valuesdecisionShell inCLIENTclock, storePure coreLLDdecides onlyShell outDBsave, send

Swipe sideways to pan · drag a box to move it

100%
The shell gathers inputs: time and stored data.

The shell reads the clock and the store.

valuesdecisionShell inCLIENTclock, storePure coreLLDdecides onlyShell outDBsave, send

Swipe sideways to pan · drag a box to move it

100%
The pure core computes the answer from plain values.

The core decides the amount.

valuesdecisionShell inCLIENTclock, storePure coreLLDdecides onlyShell outDBsave, send

Swipe sideways to pan · drag a box to move it

100%
The shell performs the effects the core asked for.

The shell then saves the change. Almost all the logic now sits in settle, which needs no mocking to test.

06

A checklist to carry forward

  • Prefer values that do not change after creation.
  • When you must mutate, do it where only one owner can see it.
  • Give functions all they need as arguments, and return results instead of editing inputs.
  • Keep reading the clock, randomness and storage at the edges of the program.

You will see this shape again in testability, in state machines, and in event driven designs.