← Fundamentals
Guided lesson 5 questions · 6 languages

Side effects and pure functions

What a function reads and changes besides its arguments and its result.

A pure function gives a result that depends only on its arguments, and it changes nothing else, so the same arguments always give the same result. A function that reads something it was not given, such as a global variable or the clock, or that changes something, such as its argument or the screen, is not pure.

Before you begin

Why this matters

Most surprising results come from a function that read or changed something you did not pass to it. If you can tell a pure function from one with side effects, you can predict a call from its arguments and test it without setting anything up.

What this helps you reason about

  • Predict when the same call gives two different answers.
  • Spot a function that changes the array you gave it.
  • Move a hidden value into a parameter so a test can choose it.

A function gives back a value, but it can also read things it was not given and change things outside itself. This lesson shows how to spot both, why they make a function hard to test, and how to write functions that depend only on their arguments.

Already know this? The short version
  • A pure function’s result depends only on its arguments, and it has no side effects; the same arguments always give the same result.
  • A hidden input, such as a top-level variable, Date.now(), Math.random(), or a file, can make one call give two answers. Pass the value in as a parameter instead.
  • An array or object argument is the caller’s, and methods such as pop, push, and sort change it. Nothing in TypeScript marks such a function; readonly array types catch some changes before the run.
  • Keep a pure core and put reading and printing at the edges: read inputs first, pass them in, print at the end. Pure functions can be tested by calling them.

Go to the final check· Go to the practice tickets

Words in this lesson (5)
side effect
A change that code makes besides giving its value, such as changing a variable or printing a line.
hidden input
A value a function reads that is not one of its arguments, such as a global variable, the current time, a random number, or a file.
pure function
A function whose result depends only on its arguments and which has no side effects. Called again with the same arguments, it gives the same result.
mutation
Changing the inside of an object, as in counter.value = 5. Every name that refers to that object sees the change. Reassignment (count = 5) is different: it changes only which value one name holds.
readonly
A TypeScript marker on a property or an array type that makes the compiler reject changes through that type. It is checked before the program runs and has no effect while it runs.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Same call

Why can the same call give two different answers?

Expressions and statements called a change that code makes besides giving its value a side effect. This lesson asks which functions have side effects, and what else a function depends on.

fare below has one parameter, stops, but it also reads pricePerStop, a variable declared at the top level of the file, as a closure reads a captured variable in Closures and captured variables. So fare(4) does not always give the same answer: 8 while pricePerStop is 2, and 12 after the program sets it to 3. The answer depends on when fare runs, not only on what it receives.

A value a function reads that is not one of its arguments is a hidden input. A function is pure when its result depends only on its arguments and it has no side effects. Then the same arguments always give the same result. fare is not pure: pricePerStop is a hidden input.

main.ts
let pricePerStop = 2;
function fare(stops: number): number {
  return stops * pricePerStop;
}
const first = fare(4); // 8
pricePerStop = 3;
const second = fare(4); // 12

A function that reads a variable it was not given can give two answers for the same arguments.

Coming from Python: As in Python, a function can read a top-level variable with no mark at all. In Python, only assigning to it needs global; in TypeScript, not even that.

Precisely: what counts as pure

A pure function may use local variables and change them: no caller can see them. Reading a constant that never changes does not make a function impure either: the value is fixed, as if written in the body. What matters is what a caller can observe. Some definitions also ask that a pure function always finish and never fail; this lesson does not.

Experiment

Call one function twice with the same argument

fare multiplies its parameter by a variable it was not given. The variable changes between the two calls, so the same argument gives two results. The result depends on when fare runs, not only on what it receives.

Why it matters When a function gives two answers for the same arguments, look for a variable it reads that is not a parameter.

Predict the two numbers. Then move the line pricePerStop = 3; up, above const first = fare(4);, and predict again.

let pricePerStop = 2;

function fare(stops: number): number {
  return stops * pricePerStop;
}

const first = fare(4);
pricePerStop = 3;
const second = fare(4);
console.log(first, second);
What to look for

Step into fare at each call. Its frame lists stops 4 both times, and also pricePerStop, the top-level variable it reads: 2 at the first call, 3 at the second. The run prints 8 12. After the edit, both calls list pricePerStop 3, and the run prints 12 12. <top level> is the stepper’s name for the code outside every function, and a const that has not run yet shows as not initialized yet. Step sometimes stops on the same line twice; that is normal for this debugger.

Expected output

8 12

02 / Arguments

Can a function change the value you gave it?

A second way to be impure is to change something the caller can see. A function that receives an array receives a reference to the caller’s array, as Arguments, values, and references showed, so the caller sees every change the function makes to it.

Some changes hide inside a method call. stops.pop() returns the last element and also removes it from the array. lastStop below looks like a question, “which is the last stop?”, but each call shortens the caller’s route, and the next call gives a different answer. Its result type, string | undefined, means a string, or undefined when the array is empty. stops[stops.length - 1] reads the last element and changes nothing.

Nothing in TypeScript marks a function that changes its argument. A readonly array type, as in stops: readonly string[], makes the compiler refuse pop, push, and sort on that parameter. It is checked only before the program runs.

main.ts
function lastStop(stops: string[]): string | undefined {
  return stops.pop();
}
const route = ["Ash", "Elm", "Oak"];
console.log(lastStop(route)); // Oak
console.log(lastStop(route)); // Elm
console.log(route.join(",")); // Ash

A function that changes its argument is not pure: the caller’s data is different after the call, and so is the next answer.

Coming from Python: As in Python, pop removes the element it returns. TypeScript has no stops[-1]; read stops[stops.length - 1].

Precisely: which array methods change the array

pop, push, shift, unshift, sort, reverse, splice, and fill change the array they are called on. map, filter, slice, and concat return a new array and leave the old one as it was.

Experiment

Ask twice for the last stop

stops holds a reference to the caller’s array. pop returns the last element and also removes it from that array, so each call changes route, and the next call finds a different last stop.

Why it matters A function that changes its argument gives a different answer the next time, and leaves the caller with different data.

Predict the three printed words. Then change return stops.pop(); to return stops[stops.length - 1]; and predict again.

function lastStop(stops: string[]): string | undefined {
  return stops.pop();
}

const route = ["Ash", "Elm", "Oak"];
const first = lastStop(route);
const second = lastStop(route);
console.log(first, second, route.join(","));
What to look for

Step into lastStop. Its frame lists stops as ["Ash", "Elm", "Oak"], and <top level> shows route with the same three. At the next stop, both show ["Ash", "Elm"]: pop changed the one array that both names reach. After the second call, both show ["Ash"]. The run prints Oak Elm Ash. After the edit, route keeps all three, and the run prints Oak Oak Ash,Elm,Oak.

Expected output

Oak Elm Ash

03 / Hidden inputs

What else can a function read or change?

Changing a variable or an argument is not the only side effect. Printing a line, writing a file, and sending data over the network are side effects too. And a top-level variable is not the only hidden input. Date.now() reads the clock: it returns the number of milliseconds since 1 January 1970. Math.random() returns a different number at each call. Reading a file or typed input gives whatever is there when the program runs.

Each of these makes a function harder to test and to reuse. A test calls a function with chosen arguments and compares the result with an answer worked out in advance. That works only when the same arguments always give the same answer. stillValid below gives a different answer at 9:00 and at 11:00 for a ticket bought at 8:00, so a test of it passes or fails depending on when it runs. A function that prints its result can be checked only by reading the screen, and code that needs the value without the printing cannot use it.

The repair for a hidden input is to pass it in. validAt takes the time as a parameter, so a test can choose any time it likes.

main.ts
// hidden input: the clock
function stillValid(boughtAt: number): boolean {
  return Date.now() - boughtAt < 90 * 60 * 1000;
}
// the time is passed in
function validAt(boughtAt: number, now: number): boolean {
  return now - boughtAt < 90 * 60 * 1000;
}

Hidden inputs and side effects make a function hard to test and reuse; pass the hidden value in as a parameter.

Precisely: reading is a side effect too

Reading typed input or a file also changes something: the reading position moves on, so the next read gets the next line. A random number generator changes its own state at every call.

04 / Edges

How do you keep side effects at the edges?

A program needs some side effects: without printing, nobody sees its result. The aim is to keep them in a few places. A common shape is a pure core with the side effects at the edges: a few lines at the start read the inputs, pure functions compute the values, and a few lines at the end print them.

In the program below, fare takes the price as a parameter, so it is pure. The top-level code reads pricePerStop, passes it in, and prints at the end. fare(4, 2) === before is a small test, and because fare is pure it gives the same answer whenever it runs.

fare’s parameter has the same name as the top-level variable, so inside fare the name means the parameter, as in Scope and shadowing. The experiment shows what happens when the parameter is renamed and the body is not.

main.ts
function fare(stops: number, pricePerStop: number): number {
  return stops * pricePerStop;
}
let pricePerStop = 2;
const before = fare(4, pricePerStop);
pricePerStop = 3;
const after = fare(4, pricePerStop);
console.log(before, after, fare(4, 2) === before);
// 8 12 true

Read inputs first, pass them to pure functions, and print at the end.

Coming from React: React expects a component’s render to be pure: the same props and state give the same output. Work such as fetching data belongs in an effect, at the edge.

Experiment

Pass the price in, then test the result

With the parameter named like the top-level variable, the body uses the parameter, so fare reads only what it is given. After the edit, the parameter is called price, and the body’s name refers to the top-level variable again: a hidden input is back, and fare(4, 2) gives 12 once the top-level value is 3.

Why it matters A function is pure only if its body uses nothing but its parameters. Check the body, not only the signature.

Predict the three printed values. Then rename the parameter pricePerStop to price in fare’s first line only, leave the body as it is, and predict again.

let pricePerStop = 2;

function fare(stops: number, pricePerStop: number): number {
  return stops * pricePerStop;
}

const before = fare(4, pricePerStop);
pricePerStop = 3;
const after = fare(4, pricePerStop);
console.log(before, after, fare(4, 2) === before);
What to look for

Each time fare runs, its frame lists stops and its parameter pricePerStop: 2, then 3, then 2 again for the call inside console.log. The run prints 8 12 true. After the edit, the frame lists price and also pricePerStop, the top-level variable. At the third call, price is 2 and pricePerStop is 3, and the run prints 8 12 false.

Expected output

8 12 true

05 / Try it

Can you find every side effect?

Find every side effect of seatsLeft, then follow the two calls. The next lesson, Arrays, lists, and vectors, looks at the lists that several functions here changed or copied.

main.ts
let calls = 0;

function seatsLeft(seats: number[], booked: number): number {
  calls = calls + 1;
  seats[0] = seats[0] - booked;
  return seats[0];
}

const carriage = [40];
const a = seatsLeft(carriage, 5);
const b = seatsLeft(carriage, 5);
console.log(a, b, carriage[0], calls);

What is printed?

Reveal the reasoning

35 30 30 2. seatsLeft has two side effects: it adds 1 to the top-level calls, and it changes the caller’s array through seats. The first call leaves 35 in carriage[0]; the second starts from 35, not 40, so the same arguments give 30. A pure version would take the free seats as a number and return the new number.

Practice tickets

  1. PURE-101 · The planned times change when a delay is shown → Free
  2. PURE-102 · Planned night trips get the day price → Sign in · Pro
  3. PURE-103 · A trip keeps its old fare after the price per stop goes up → Sign in · Pro

Keep exploring

Arrays, lists, and vectors →

Primary references