A function that decides whether a subscription renews reads the clock itself — what does that cost you?
answer
- two calls, two answers
- time is an input, not a lookup
- the signature is lying
- read the clock once at the edge
- pass the instant, not the clock
basics
~20 sReading the clock inside the decision turns time into an undeclared input: the same subscription record can produce two different answers, and no test can pin the answer down. Pass the instant in as a parameter.
solid answer
~40 sThe function really has two inputs — the subscription record and the instant it happened to run at — and only the first is in the signature. That makes it impure: same arguments, different result. A test then has to move the machine clock or accept a case that passes today and fails on a renewal boundary, and a wrong decision in production cannot be replayed because the input was never recorded. It also lets two reads inside one run straddle a boundary and disagree. The fix is to make the hidden input visible: take `now` as a parameter, read the clock once at the outer edge of the program, and hand that same instant to every decision in the run.
code
pseudocode · 14 lines// hidden input: the decision reaches for the world itself
function shouldRenew(subscription)
now = currentInstant() // nothing in the signature says this
return subscription.paidThrough < now
// declared input: the caller supplies the instant
function shouldRenew(subscription, now)
return subscription.paidThrough < now
// the edge takes one reading for the whole run
function runRenewals()
now = currentInstant()
for each s in dueSubscriptions
if shouldRenew(s, now) then charge(s)go deeper
Recall both halves of the definition and apply them: a pure function returns the same result for the same arguments and changes nothing observable. A clock read breaks the first half even though it writes nothing.
Explain that the instant is an undeclared parameter, and show the rewritten signature that takes it. Say why the reading is taken once per run rather than once per decision.
Talk about the incident you could not reproduce because the deciding input was never recorded. Passing the instant in means a failing run replays exactly from the value in the log.
Frame it as a convention for other teams: name the places in the system allowed to read a clock at all, and weigh the reproducibility that buys against the cost of threading one more argument through every layer.
## Two inputs, one of them undeclared A function is **pure** when two things hold: the same arguments always produce the same result, and the call changes nothing anyone else can observe. A renewal check that reads the current time writes nothing at all, so it is tempting to call it pure. It is not, and the half it fails is the first one. The honest input list of `shouldRenew(subscription)` is the subscription record **and the instant the call happened to run at**. Only the record appears in the signature. The instant arrives through a side door. That is a **hidden input**: a value the result depends on that the caller cannot see, cannot set and cannot record. Hidden inputs cost three concrete things: - **Determinism.** Two calls with an identical record can disagree, and nothing in the code explains why. - **A cheap test.** To exercise "the day after the paid-through date", the test must move the machine's clock or wait for the calendar to reach it. - **Reproduction.** When a decision was wrong in production, the value that produced it was never written down, so there is nothing to replay. ## Promoting the hidden input The repair is small and mechanical: 1. Find every reach for the outside world inside the decision — the clock, a random draw, an environment lookup, a counter that advances. 2. Add it to the parameter list as a **value**, not as a source of values. 3. Take the reading once, at the outer edge of the program, where the run begins. 4. Hand that one reading down to every decision the run makes. Step 4 carries more weight than it looks. If each decision reads the clock for itself, two decisions in one run can land on either side of a boundary — a renewal date, a period rollover — and contradict each other for a reason no reader can see in the code. One instant per run makes the run internally consistent, and a log line recording that instant is then enough to recompute every decision the run made. ## Passing a value is not the same as passing a source | arrangement | same arguments, same result | test needs the clock controlled | two reads can disagree | |---|---|---|---| | reads the clock inside itself | no | yes, or the test waits | yes | | is handed something it can ask for the time | no | yes, a stand-in is supplied | yes | | is handed the instant as a value | **yes** | no | no | Being handed an object it can ask for the time is a real improvement: a test can supply a stand-in and the case becomes repeatable. But the function still returns different answers for the same arguments across calls, because one of its arguments is a door to the outside rather than a value. It has become **controllable**; it is not yet **pure**. The distinction is worth keeping, because the payoffs people actually want here — replaying a run from a log, reasoning about a decision from its arguments alone, running cases in any order — come from purity, not from controllability. ## Randomness has the same shape Anything the decision samples rather than receives is this defect in different clothes: a random draw, a freshly generated identifier, a counter, a lookup of the current locale. Two treatments keep the deciding function pure: - **Draw at the edge.** The outer layer takes the values it needs and passes them in, and the decision is a plain calculation over them. - **Thread a seed.** The decision takes a seed and returns the drawn value together with the next seed, so the whole sequence is a function of what came in. What does not work is being handed a generator that advances internal state on every call. That is the clock again: the argument is a source, its state changes between calls, and identical arguments yield different results. ## What this actually buys It does not make the program do less I/O — the clock is still read, exactly once, somewhere. It relocates the read to a place where it is visible and recordable, and leaves the decision as a calculation whose behaviour is fully determined by the values on its argument list. That is what lets a test state a case as "this record, this instant, expect this" with no fixture at all, and what lets a bad production run be replayed exactly from the arguments it was given. One last caution about scope: promoting the instant fixes the decision, not the program. Somebody still reads the clock, and that somebody is now the outer layer, whose own correctness has to be checked in a slower way.
- If the outer layer hands the decision an object it can ask for the current time, is the decision pure?No — it is controllable, not pure. Asking the object still returns a different value on each call, so the same arguments can produce two answers. You have made the dependency visible and substitutable, which is enough for a repeatable test, but the result still depends on something outside the argument list.
- Why read the clock once per run instead of once per decision?Two reads in one run can fall on either side of a boundary — a renewal date, a period rollover — so decisions that should agree disagree, and nothing in the code explains it. A single instant taken at the edge makes the run internally consistent and lets the whole run be recomputed later from one recorded value.
- How does the same treatment apply to randomness?Identically. Either draw the values at the edge and pass them in, or pass a seed and use a routine that returns the drawn value together with the next seed. What fails is a generator that advances hidden state on each call: the argument is a source rather than a value, so identical arguments still give different results.
saying these in an interview costs you the question
- Claims the function is pure because it writes nothing
- Says one clock read is too small to affect purity
- Wants the test to wait for or move the machine clock
- Reads the clock twice in one run and expects agreement
- Thinks being handed a clock object already makes it pure