How does handing a runner a value that describes an action differ from simply calling the function later?
answer
- both delay it; only one is readable
- invocation is the single interpreter
- data has many interpreters
- dry run, record, retry, perform
- one runner decides, not every holder
basics
~20 sA postponed call supports exactly one operation — invoking it. A description is data with a fixed set of cases, so several different runners can interpret the same value: one that performs it, one that prints it, one that records or retries it.
solid answer
~40 sBoth delay the effect, so the difference is not *when* it happens but *what else you can do with it in the meantime*. A postponed call is opaque: whoever holds it can only fire it, and every holder decides for itself when. A described action is a value built from known cases, so it can be read as well as run — a reporting runner prints the install plan, a recording runner captures it in a test, a retrying runner applies it again after a failure, and the real runner performs it. That is the point of the technique: one description, several interpretations, and a single place in the program that chooses the interpretation that actually touches the world.
code
pseudocode · 10 lines// postponed call: the one thing available is invoking it
later = function() { write(target, bytes) }
later() // no way to ask what it would have done
// described action: a value other code can read
action = Write(target, bytes)
report(action) // "would write 412 bytes to target"
recorded = record(action) // a test collects it instead of writing
perform(action) // the runner that actually writesgo deeper
Remember the asymmetry: a postponed function can only be called, while a described action can also be read. Both wait; only one can be examined first.
Explain the mechanism — a description is built from known cases, so any walker of those cases is an interpreter, and performing is only one of them. Name two others you would write.
Show where the second interpreter pays in a running system: an operator preview before a destructive change, an audit line, a retrying runner, a test that asserts on plans instead of fakes.
Judge the tax. Every case in the description must be handled by every interpreter the team maintains, so decide which effects deserve to be data and which stay as ordinary calls.
## The question behind the question When someone first meets actions-as-values, the natural objection is that a closure already defers work: write `later = function() { fetchAndInstall("parser") }`, hold it, invoke it at the edge, done. The objection is fair about timing. Both shapes postpone the effect to a moment the program chooses. The difference is not timing. It is **how many things can be done with the postponed work**. ## One interpreter versus many A postponed call has exactly one interpreter: invocation. You cannot ask it what it would do, because the only way to find out is to let it do it. Its type says something like "give me nothing, get back nothing", which is the least informative description available. A described action is data built from named cases — `Fetch`, `Unpack`, `Link`, a sequence of them. Anything able to walk those cases is an interpreter, and you can write as many as you need: - **The performing runner** — walks the plan and does the work. - **A reporting runner** — walks the same plan and prints "would fetch, would unpack, would link" for an operator to approve. - **A recording runner** — used in tests, returns canned outcomes and collects what was asked for, so the test asserts on a list rather than on a fake filesystem. - **A retrying or rate-limiting runner** — applies the plan, and on failure applies the remaining part again, with a delay it chooses. - **A checking runner** — refuses a plan whose steps the current operator is not permitted to perform. None of these requires a change to the builder. That is the leverage, and it is unavailable to a postponed call, which can only be invoked or dropped. ## Who decides when it runs There is a second, quieter difference. A postponed call is invokable by any code that holds it, so the decision "when do effects happen here" is spread across every holder, and the reader of any one module cannot tell whether calling a function it received will write to disk. With descriptions the convention is that a value is inert and one runner applies it, so the decision lives in one place that is easy to name and easy to review. | | a postponed call | a described action | |---|---|---| | operations available | invoke it | run, print, record, rewrite, compose, compare | | what a holder can learn | its type, and nothing more | its steps, by walking them | | who decides when effects occur | every holder | the one runner | | test strategy | substitute fakes underneath it | compare the value that was built | | cost | none — it is just a function | a case set and an interpreter to maintain | ## The honest middle ground Some designs use action values that *are* opaque callables — the value is a function the runner invokes, and the runner is still the single edge. This keeps two of the benefits: composition (several actions combine into one) and the single-runner discipline. It gives up the third, inspection: nothing can report on the plan, diff two plans, or drop a step, because there are no steps to see, only a function. So the practical rule is about what you want back: 1. If you only need the effect to happen at a chosen moment, a postponed call is sufficient and cheaper. 2. If you need composition and one place that decides when effects happen, an opaque action value is enough. 3. If you need a second interpretation of the same work — a dry run, an audit line, a rewrite, a recorded test — the description must be data, and the effort of a case set is what buys it. ## What it does not change Describing an action does not make it asynchronous, does not make it cancellable, and does not make it faster. It also does not make the work optional: if the plan reaches the runner, the runner performs it. The technique changes what the program can *say about* the work before it happens, not the work itself.
- Is an action value that is just an opaque callable still worth the ceremony?Often yes, for two of the three benefits: several actions still compose into one, and one runner still owns when effects happen. What you lose is inspection — nothing can print, diff or rewrite the plan, because it has no visible steps. Choose a case set only when a second interpretation actually pays for itself.
- What does a recording interpreter give a test that a substituted fake does not?It moves the assertion from behaviour to value. Instead of wiring fakes underneath the code and checking they were poked, the test builds the plan and compares it with the expected steps. The test then fails on a wrong plan rather than on the mechanics of the fakes.
- Why is a fixed set of cases part of the cost rather than part of the benefit?A description can express only what its cases allow. Every new kind of effect means a new case and a matching arm in every interpreter you maintain. That is a real tax, and it is why teams describe the effects they want to preview or audit rather than all of them.
saying these in an interview costs you the question
- Says a postponed call and a described action are the same thing
- Claims a description performs itself when execution reaches it
- Thinks deferring a call gives you a dry-run report
- Believes an opaque action value can be inspected step by step
- Assumes only one interpretation of a description is possible
- Says describing an action makes it asynchronous