skip to content

Every line of the fee calculator is arithmetic, yet the audit calls it impure — how can that be?

level: seniorimportance: should knowfreq 38%

answer

  1. purity is not a local property
  2. effects propagate upward through callers
  3. as pure as the least pure callee
  4. audit the call graph, not the body
  5. a dynamic call yields a conditional verdict

basics

~20 s

Effects are inherited through calls. A function is at most as pure as everything it calls, so a clock read, a configuration lookup or a shared write hiding one level down in a helper makes the arithmetic above it impure too.

solid answer

~50 s

Purity is a property of the whole call graph reachable from the body, not of the lines you can see. If the fee calculator asks a helper for today's rate card, and that helper reads the clock, then the calculator has an undeclared input even though no line of it mentions time. The same goes for a helper that bumps a counter or fills a shared cache. This is why reviewing a single function is never conclusive: you can only decide purity after auditing everything it calls, down to functions that read nothing but their parameters. Two cases stop the audit cold — a call made through a function-valued parameter, and a call through an abstraction whose implementation is chosen at run time. For those the honest verdict is conditional: pure **given** that what arrives is pure.

code

pseudocode · 9 lines
pseudocode
function deliveryFee(order)
    rates = activeRateCard()              // every effect below arrives here
    weight = totalWeight(order.lines)     // pure: arithmetic over the argument
    return rates.base + rates.perKilo * weight

function activeRateCard()
    if now() before SEASON_END            // hidden input: nothing was passed in
        return SUMMER_CARD
    return WINTER_CARD

go deeper

for a junior

Remember that calling an impure function makes the caller impure too, so a body full of arithmetic proves nothing until you know what its helpers do.

for a middle

Explain the audit as a walk over the call graph, and name the usual hiding places: a helper resolving the current configuration, a computed default, a lazily filled shared field.

for a senior

Show you handle the calls you cannot follow — through a function-valued parameter or an abstraction resolved at run time — by recording a conditional verdict instead of guessing, and that you know a guarantee can be lost from below.

for a principal

The question you own is how the codebase keeps such a guarantee from decaying: where the condition is written down, and what stops a helper three levels down from quietly acquiring an effect.

## Purity is a property of the call graph The two halves of purity — result fixed by the arguments, nothing observable changed — are stated about the function, but they are satisfied or broken by everything the function sets in motion. Calling an impure function makes you impure, because its read is now your read and its write is now your write. There is no boundary that contains an effect: effects propagate **upward** through every caller, all the way to the top of the stack. So the working rule is: **a function is at most as pure as the least pure thing it calls.** A body of pure arithmetic over its parameters, with one call to a helper that reads the clock, is a function that reads the clock. The arithmetic is not evidence; the call is. ## Where the hidden input actually lives In a real service the entry is almost never on a line that looks like an effect. The recurring hiding places: - a helper that resolves "the current" something — the active rate card, today's tariff, the live configuration - a default argument whose value is computed rather than constant, so it can itself read something external - a lazily filled field on a shared object, where the read looks ordinary and the fill happened in an earlier call - a validation or formatting utility that logs on the failure path only, so the effect fires rarely enough to survive review - an identifier or correlation value generated inside a helper for tracing Each of these reads like plumbing rather than like an effect, which is exactly why they survive. The reviewer's eye is trained to look for a sink — a store, a stream, a queue — and none of these names one. ## What a body alone can and cannot tell you | What the body does | Decidable from the body? | Why | |---|---|---| | arithmetic over parameters and locals only | yes | every name is a parameter or created here | | calls a named helper you can also read | yes, after auditing the helper | the audit simply continues one level down | | calls through a parameter that is itself a function | no — conditional only | the call site decides what runs | | calls through an abstraction chosen at run time | no | the implementation is not fixed by the text | | reads a value some earlier call left on shared state | no, until you find the writer | the read looks local; its source is not | The third and fourth rows are where seniority shows. The right answer is not "assume the worst" and not "assume it is fine" — it is to state the verdict as a condition. A higher-order fee calculator that applies a discount function supplied by its caller is pure **whenever the supplied function is pure**, and that guarantee belongs to whoever assembles the two. If the type system or the contract cannot express that condition, then it is a convention, and conventions are worth writing down next to the function. ## Why this is the review habit that matters The line-by-line audit of a single function is the first skill; knowing that it is not sufficient is the second. Three practical consequences: 1. **A passing unit test proves nothing about purity.** A test that calls the function once, with the clock reading whatever it read that morning, is perfectly happy. The effect shows up as the odd failure months later. 2. **Purity decays silently.** A function audited as effect-free stays that way only until someone adds a log line to a helper three levels down. Nothing at the original site changes, and the guarantee everyone relies on is gone. 3. **Anything that relied on the guarantee inherits the breakage.** Whatever the codebase does because a function was believed effect-free now rests on something untrue, and the failure surfaces far from the helper that changed. ## Running the audit properly 1. Read the body and list every call it makes, not just every read and write. 2. For each callee, repeat the audit, until you reach functions that touch nothing but their parameters and their locals. 3. Mark any call you cannot follow — through a function-valued parameter, or through an abstraction resolved at run time — and record the verdict as conditional on what arrives there. 4. Write the finished verdict where the next reader will see it, with the condition attached if there is one. The sentence to leave the interview with: **you cannot read purity off a body; you read it off everything the body reaches.** That is what makes the clock read in a two-line helper more dangerous than a log statement in the function under review — the log statement is visible, and the helper is one level past where most people stop looking.

  • A parameter of the fee calculator is itself a function supplied by the caller — what can the audit conclude?
    Only a conditional verdict: the body is pure given a pure argument. The effects, if any, enter at the call site, so the guarantee belongs to whoever composes the two. Record the condition rather than guessing a yes or a no.
  • Which is easier to spot in review: an outward write buried in a helper, or a hidden input?
    The outward write, usually — it names a sink, so a search for sinks finds it. A hidden input reads as an ordinary value, so nothing marks it as special. That is why clock, configuration and identifier reads dominate the effects that survive review.
  • A function audited as effect-free last quarter is impure today with no change at its call site. How?
    Someone added an effect to something it calls. Purity is inherited, so it can be lost from below without a single line changing in the function that loses it. Anything relying on the old guarantee is now relying on something untrue.

saying these in an interview costs you the question

  • Believes a body with no visible I/O is automatically pure
  • Audits the function under review but never the helpers it calls
  • Assumes a function-valued parameter is always effect-free
  • Forgets a computed default argument can read something external
  • Calls a function pure because its unit test passed
  • Treats an unfollowable call as pure rather than as a conditional verdict