skip to content

Substitution & Equational Reasoning

The property that lets you swap a call for its value and reason about code like algebra, plus the refactors it makes safe. Interviewers use it to test whether purity is more than a slogan.

on this pageshow

questions

4

In a payroll calculation, what does it mean that a call may be replaced by its result?

level: juniorimportance: must knowfreq 72%

answer

  1. read the code like algebra
  2. a call and the value it produced
  3. both directions, not just one
  4. meaning of the whole program unchanged
  5. equals replaced by equals

basics

~10 s

It means the call is referentially transparent: anywhere the call appears, writing the value it returned instead leaves the meaning of the program unchanged, and putting the call back is equally safe.

solid answer

~50 s

It means the expression is **referentially transparent**. If `tax(gross)` evaluated to `900`, then writing `900` wherever that call appears leaves the program computing exactly the same payslips — and the reverse swap, replacing the literal `900` with the call that produces it, is just as safe. That two-way freedom is why people say pure code reads like algebra: you may replace equals by equals and nothing else on the page shifts. It holds wherever the value you write down is the same value the call would produce — the function must return the same result for the same arguments, evaluating it must not change anything anyone else can observe, and the result must not be something a caller can alter afterwards and notice the difference. Strip any of those away and the two programs stop meaning the same thing.

code

pseudocode · 8 lines
pseudocode
function tax(gross)          // same in, same out, nothing else happens
    return gross * 0.18      // flat rate, for this example

gross = 5000

net = gross - tax(gross)     // tax(gross) evaluates to 900
net = gross - 900            // call replaced by its value
net = gross - tax(gross)     // and replaced back again

go deeper

for a junior

Be able to say it plainly: if the call produced 900, writing 900 there instead changes nothing, and writing the call back is equally fine. Then give one call where that fails.

for a middle

Explain the three conditions behind the swap — same arguments give the same value, evaluating it changes nothing observable, and the result cannot be altered afterwards — and show which one a given call breaks.

for a senior

Show what it buys in review: rewrites that move a call around become safe from the fragment alone, without reading every caller first. Name the case in your own code where that licence did not hold.

for a principal

The judgment is where to draw the line so the licence holds broadly: which layers must stay substitutable, what teams may put behind a call, and what you give up in convenience to keep the property.

## The property in one sentence A call is **referentially transparent** when writing the value it produced in its place leaves the meaning of the surrounding program unchanged. Payroll makes it concrete. If `tax(gross)` evaluates to `900`, then in that same context every occurrence of `tax(gross)` may be written as `900`: the payslips come out identical, the rest of the run is untouched, and nothing downstream can tell which version you shipped. This is exactly the licence algebra gives you — replace equals by equals — and it is the whole reason purity is worth paying for. A language cannot grant it by itself; the function has to earn it. Note that the property belongs to an **expression**, not to a name. `tax` is not "transparent"; the expression `tax(gross)` is, in a context where `gross` denotes a fixed value. ## Both directions are licensed People usually state the swap one way and then use it the other way without noticing. Both are legal: - **Call to value.** This is evaluation, and it is what lets you read a program by simplifying it: replace the call by what it produced and keep going until a number is left. - **Value to call.** This is the direction that makes refactoring safe. Naming a repeated subexpression, extracting a helper, or folding a literal back into the definition that computes it are all this swap. - **Value to a different call.** If two expressions denote the same value, either may stand where the other stands — this is what makes one implementation replaceable by another without reading its callers. A candidate who only knows the forward direction can evaluate but cannot explain why the refactor they just did was safe. ## What has to hold for the swap to be legal 1. **Same arguments, same value, every time.** If a second evaluation could disagree with the first — because the function consults something that moves, or hands out a fresh identifier each call — there is no single value to write down, and the swap is meaningless before it is wrong. 2. **Evaluating it changes nothing observable.** If running the call also makes something happen that the rest of the system can detect, then writing the value down deletes that happening. The programs now differ, even though the arithmetic agrees. 3. **The value cannot be altered underneath you.** If the call returns a structure and callers modify it, then `n` separate evaluations gave `n` independent structures while one substituted value gives one shared structure. The swap silently changed the aliasing. ## A worked swap With `gross = 5000` and a flat 18% rate, `tax(gross)` evaluates to `900`. So these three lines all mean the same thing: - `net = gross - tax(gross)` - `net = 5000 - tax(5000)` - `net = 5000 - 900` You can walk down that list (evaluating) or back up it (abstracting). Neither move needs you to know anything about the code around it — which is the practical payoff: substitution is a **local** licence that stays valid no matter how large the program gets. ## Where the swap fails | the call | replace it with its value? | why | |---|---|---| | a rate looked up from its arguments | yes | successive evaluations agree and nothing else happens | | one that appends a line to an audit trail | no | the line stops being appended when you write the number down | | one that hands out the next payslip number | no | two evaluations disagree, so there is no "its value" | | one that reads a value that changes during the run | no | the answer depends on when you asked | | one returning a list the caller then edits | only if callers cannot edit it | one shared list replaces several independent ones | The honest summary is that the swap is a property you check, not a default you assume — and the check is about what evaluating the call does, not about what type it returns. ## Why interviewers open here Because it separates candidates who treat purity as a slogan from those who use it. The follow-up is almost always "so what does that buy you?", and the answer is that every rewrite which amounts to moving a call around — naming it, inlining it, computing it once instead of many times — becomes safe by construction rather than by inspection. Without the property you must read every caller before you move anything; with it you may reason about the fragment in front of you and stop.

  • Is this the same as saying the function is deterministic?
    Determinism is only the first half: the same arguments give the same result. Substitution also needs that evaluating the call changes nothing anyone else can observe, and that the returned value cannot be altered afterwards in a way a second evaluation would not have shared. A deterministic function that also records every call it received is deterministic and not substitutable.
  • Does the swap still hold if the call returns a structure the caller then modifies?
    Not reliably. Three evaluations produce three independent structures; one substituted value produces one structure shared by all three use sites. If callers only read it, the programs agree. If any caller modifies it, they diverge — so the swap needs the result to be unmodifiable, or every caller to treat it that way.
  • If a call is substitutable, does that say anything about how fast the program runs?
    It says the meaning is unchanged, not the cost. Writing the value down replaces an evaluation with a literal, so the rewritten program may do strictly less work; going the other way may do more. Substitution is a licence about meaning — the cost difference is a separate thing you then choose to exploit.

A recipe is a fixed instruction: reading it out loud, or writing down the dish it yields, tells you the same thing. A pot being stirred is not — what you find in it depends on when you looked.

saying these in an interview costs you the question

  • Thinks any call can be swapped for its value, purity or not
  • Says the swap only works one way, call to value
  • Treats the return type as proof the call is substitutable
  • Claims a call that also writes an audit line is fine because the number is the same
  • Calls it a compiler optimisation rather than something a reader may use
open as a page

How do you evaluate a nested payroll expression by the substitution model, one step at a time?

level: middleimportance: should knowfreq 58%

basics

~20 s

Replace a call with its body, substituting the argument values for the parameters, then reduce what results; repeat until only a value is left. Every rewrite preserves meaning, so the final value is the expression's value.

open as a page

Two teams computed the same payroll deduction with different expressions — how do you prove the two always agree?

level: middleimportance: should knowfreq 40%

basics

~20 s

Unfold both expressions by substituting their definitions, then rewrite each using laws that genuinely hold for the operations involved until both reach a common form. Equal forms prove agreement for every input the laws cover.

open as a page

A reviewer hoists a repeated call out of a payroll loop — what makes that rewrite meaning-preserving?

level: seniorimportance: should knowfreq 46%

basics

~10 s

It preserves meaning when the call's arguments do not vary across iterations and the function is pure, so every iteration would have produced the same value and evaluating it once loses nothing observable.

open as a page