skip to content

Why can an action value be duplicated, stored and compared freely, while applying that same action twice is not safe?

level: seniorimportance: should knowfreq 40%

answer

  1. substitution holds up to the runner
  2. building is pure, applying is not
  3. a plan is not a cached result
  4. two applications, two installations
  5. repetition tolerated only by design

basics

~20 s

The description is produced by a pure function, so it can be replaced by its definition anywhere without changing the program's meaning. Applying it is where effects occur, and two applications perform the work twice — the value is a plan, never a cached result.

solid answer

~40 s

Substitution holds where nothing observable changes. Building an install plan changes nothing, so `plan` and `installPlan("parser")` are interchangeable: share one value, build it twice, cache it by its inputs — the program cannot tell. Applying the plan is the opposite: each application fetches, unpacks and links again, so replacing two applications with one, or one with two, changes the world. The trap candidates fall into is treating the plan as though it held a result — as if a second application would return what the first produced. It does not; a description carries what to do, and a runner that retries will genuinely do it again unless the steps were designed to tolerate repetition.

code

pseudocode · 8 lines
pseudocode
planA = installPlan("parser")
planB = installPlan("parser")

equal(planA, planB)    // true: the builder is a pure function
store(planA)           // safe: it is ordinary data

apply(planA)           // fetch, unpack, link
apply(planA)           // all three happen again - not a cached result

go deeper

for a junior

Hold on to the asymmetry: copying or rebuilding a plan is free, but each time a runner applies one, the described work really happens again.

for a middle

Explain why substitution holds for the builder and fails for the runner, and show the consequence — two applications of one plan install twice, because a description is not a stored result.

for a senior

Demonstrate operating judgment: what a retry does to a half-applied plan, where the resume point lives, and how you catch the plan that was built, logged and never applied.

for a principal

Frame the standard you would set — which described steps must tolerate repetition, who owns the resume semantics, and what evidence a review needs before a plan may be reapplied in production.

## Where substitution holds and where it stops An expression is substitutable when replacing it with its definition — or with an equal value — leaves the program's observable behaviour unchanged. That property is what makes refactoring safe: hoisting a subexpression, sharing one value between two call sites, caching by arguments. Describing an action as a value splits a program into a region where that property holds and a region where it does not, and puts the boundary somewhere you can point at. - **Building** an action value is a pure computation. `installPlan("parser")` yields a plan and touches nothing, so the call is substitutable in both directions: one call can become two, two can become one, the result can be cached, memoised, shared between threads, sent through a queue and rebuilt on the other side. - **Applying** an action value is the effect. `apply(plan)` is not substitutable at all, because its whole purpose is that the world afterwards differs from the world before. ## The trap: a plan is not a result The most common error is to reason about an action value the way one reasons about a computed value. If `total` holds the sum of a list, using it twice costs nothing and means nothing extra. If `plan` holds an install description, applying it twice performs the installation twice: ``` planA = installPlan("parser") planB = installPlan("parser") equal(planA, planB) // true - building is a pure function apply(planA) // fetch, unpack, link apply(planA) // fetch, unpack, link again ``` The two plans being equal says something about the descriptions, not about their effects. Equal descriptions do not merge, deduplicate or cancel each other; they are simply two identical instructions, and a runner given both will follow both. ## What is safe to do with each | operation | on the description | on the application | |---|---|---| | perform it twice | free, and indistinguishable from once | performs the effects twice | | cache it by its inputs | safe, unobservable | meaningless — the effect already happened | | share one value across call sites | safe | changes how many times effects occur | | compare two of them | safe, ordinary equality | not a comparison at all | | reorder relative to other pure code | safe | ordering is part of the meaning | | drop it because the result is unused | safe | drops the effects, changing behaviour | The last row is worth dwelling on. Dropping an unused pure computation is invisible. Dropping an application removes the work. And the mirror of it is the bug people actually ship: a plan is built, transformed, logged — and never applied. The compiler is content, the tests that assert on the plan pass, and nothing installs anything. Building is so quiet that forgetting to run is a silent failure mode of this style. ## Consequences for the runner Because execution is not substitutable, every capability people want from a runner runs into the same question: how many times will a step actually be performed? 1. **Retry.** A runner that reapplies a failed plan may perform a step that had already succeeded before the failure. Either the steps tolerate repetition, or the runner must know where it stopped. 2. **Repair after partial failure.** If fetch and unpack succeeded and link failed, the world is in a state neither the plan nor the original caller described. Deciding whether to roll back, resume or fail loudly is a runner decision, and describing the plan does not make it for you. 3. **Two runs of the same plan.** Applying a stored plan again later is a legitimate feature — replaying a migration, reinstalling after a wipe — but only if each described step has been designed for it. The useful phrase in an interview is that **substitutability of the description does not transfer to its execution**. Repetition is the default behaviour of applying a plan twice; tolerating repetition is a property you deliberately design into each step, by making it check-then-act, by keying it on an identity the runner can recognise as already done, or by describing the desired end state rather than the change. ## Why the split is still worth having Even though the effectful half loses every reasoning convenience, pushing all the decision-making into the substitutable half is the point. Choosing the steps, merging two plans, dropping work already done — all of that stays in the region where refactoring is safe and equality means something. What remains on the other side is a short walk over a value, performed once, in one place.

  • If descriptions are substitutable, is it safe to memoise the builder by its arguments?
    Yes. Building performs nothing observable, so returning a previously built plan for the same inputs is invisible to the program. Memoising the *application* is a different decision entirely: the effects of the first run already happened, and skipping the second is a behaviour change you must justify.
  • A runner retries a plan whose fetch and unpack succeeded before the link failed. What must be true?
    Either each step tolerates being performed again — fetch finds the archive present, unpack overwrites the same directory, link is check-then-act — or the runner tracks where it stopped and resumes from there. Retrying an arbitrary plan is only safe when one of those two was designed in.
  • What silent failure does this style introduce that direct calls do not have?
    Building and never applying. A plan can be constructed, transformed and logged while nothing runs, and every type checks out. Direct calls cannot fail this way because the call is the work. Teams mitigate it by making the runner the only sink for plans and by asserting that one exists on each path.

saying these in an interview costs you the question

  • Says applying the same plan twice is a harmless no-op
  • Thinks the plan caches the result of its first application
  • Claims the program stays pure including the runner
  • Believes two equal plans merge into one installation
  • Assumes retrying a plan is always safe
  • Says a built plan runs itself if nothing applies it