Dataslope logoDataslope

Pure Functions

The smallest, most important idea in functional programming — and why every later chapter depends on it.

If you remember only one idea from this entire course, make it this one: a pure function is a function whose output depends only on its inputs, and which has no observable side effects.

That's it. Pure functions are the atom of functional programming. Composition, immutability, type-driven design, parallelism, testing, caching — they all get easier when the building blocks are pure.

A definition you can hold in your head

A function f is pure if and only if:

  1. Same input → same output. Given the same arguments, f always returns the same value. Forever. Across machines, threads, and times of day.
  2. No side effects. f does not write to disk, print, mutate shared state, mutate its arguments, throw on the clock, send a network request, increment a global counter, read the time, or read from a random number generator.

The dashed arrows are what makes g impure: it reaches outside the box.

Pure vs impure: side by side

Code Block
TypeScript 5.7

add and doubled are pure. nowMs, greet, and appendOne are not — and each is impure in a different way.

The four superpowers of pure functions

Pure functions are not an aesthetic preference. They have concrete, measurable advantages.

1. They're trivially testable

A pure function's test is just expected === f(input). No setup, no mocks, no teardown.

Code Block
TypeScript 5.7

Contrast with testing an impure method that writes to a database: you have to spin up a fixture, manage the connection, clean up between tests, and reason about ordering. Pure tests are free.

2. They're safe to run in parallel

Two threads (or web workers, or async tasks) calling a pure function with the same input cannot interfere — there's nothing shared to interfere with. This is why functional pipelines parallelize so easily.

3. They're cacheable (memoizable)

f(x) always returns the same value, so you can compute it once and remember it. This is called memoization.

Code Block
TypeScript 5.7

You can only do this for pure functions. Memoizing an impure function changes its behavior — you might cache "what time it is".

4. They're easy to reason about

You can read a pure function top to bottom, and once you understand it, you understand it forever. It doesn't matter who calls it, when, in what order, or with what state. The function is a self-contained mathematical mapping.

Spotting impurity in real code

Most "impurity" in everyday TypeScript falls into a small number of categories. Learn to spot them:

PatternWhy it's impure
console.log(...), console.error(...)Writes to stdout
fs.readFileSync(...), fetch(...)Reads from disk / network
Date.now(), new Date()Hidden input (the clock)
Math.random(), crypto.randomUUID()Hidden input (RNG state)
arr.push(x) on a parameterMutates input
let counter = 0; counter++; (module-scope)Mutates global
throw new Error(...) on some inputsSubtle: same input → no return value

The last one is interesting. A function that throws for some inputs is technically impure (its output is not a value for those inputs), but in practice we tolerate it for argument validation. We'll see in Functional Error Handling how to model "this function can fail" in the type and avoid throwing entirely.

"But my program has to print things"

Of course. A program that does nothing but compute pure values is useless. Real programs read input, do I/O, and produce output. The functional answer is not "no side effects ever". It is:

Push side effects to the edges of the program. Keep the core pure.

This is sometimes called the functional core, imperative shell pattern.

In a pipeline, the transformations (filter, map, reduce, compose) are pure. The materialization at the end (write to file, push to DOM, send to network) is the impure shell.

Refactoring an impure function to a pure one

A short, realistic refactor: turning a function that does its own I/O and time-reading into a pure value-builder plus a tiny shell.

Code Block
TypeScript 5.7
Code Block
TypeScript 5.7

The pure version is longer. It's also testable, deterministic, memoizable, and trivially serializable. The "hidden inputs" of the impure version (the clock and the RNG) became explicit parameters. That's the trade.

A multi-file challenge

You'll implement two small pure functions and a small impure "driver" that uses them. The functions are pure; the driver does the I/O. This is the functional-core / imperative-shell pattern in miniature.

Challenge
TypeScript 5.7
Pure core, impure shell

Implement two pure functions in stats.ts:

  • sumOfSquares(xs: readonly number[]): number — the sum of the squares of its inputs.
  • meanOfSquares(xs: readonly number[]): number — the average of the squares of its inputs, or 0 for an empty input.

Do not print, mutate the input, or read external state. The main.ts shell calls them and prints the results.

Expected output

sum=55
mean=11

QuestionSelect one

Which of these TypeScript functions is pure?

const roll = (): number => Math.floor(Math.random() * 6) + 1;

const triple = (x: number): number => x * 3;

const logAndReturn = (x: number): number => { console.log(x); return x; };

const push = (xs: number[], x: number): void => { xs.push(x); };

On this page