skip to content

An installer builds a value describing fetch, unpack and link instead of performing them — what does that buy the calling code?

level: middleimportance: must knowfreq 58%

answer

  1. the function returns a plan
  2. building performs no effects
  3. pure builder, effectful runner
  4. data you can inspect and rewrite
  5. effects only when the runner walks it

basics

~20 s

Building the plan performs no effects, so the code that assembles fetch, unpack and link can be tested, inspected, combined and rewritten as ordinary data. Nothing reaches the network or the disk until a runner is handed the value.

solid answer

~40 s

The installer function returns a description — a value whose cases say `Fetch`, `Unpack`, `Link` — rather than doing the work. Because building it performs no effects, calling it twice yields two equal plans and zero downloads, so a test can assert on the structure without a network. The plan is ordinary data until execution, which means other code can read it: a reporter can print what would happen, a rewriter can drop a step already satisfied, a composer can splice two plans together. Effects appear only when a runner walks the value. The payoff is that the interesting decisions — which steps, in which order — become a pure computation you can check, and the irreducibly effectful part shrinks to one runner.

code

pseudocode · 12 lines
pseudocode
function installPlan(name)
  return Sequence([
    Fetch(name),
    Unpack(name),
    Link(name)
  ])

plan = installPlan("parser")      // three step values; no download yet
plan = dropStepsAlreadyDone(plan) // still a value transformation
report(plan)                      // prints what would happen

runner.apply(plan)                // the one place effects occur

go deeper

for a junior

Recall the one-line distinction: the function returns a description of the work, and the work happens only when something else is handed that description.

for a middle

Explain the mechanics — the builder is a pure function from inputs to a plan, the plan is data you can compare or transform, and one runner performs the steps. Name what a test of the builder needs.

for a senior

Show the judgment: which parts of a real service benefit from an inspectable plan, how you keep the builder from quietly reading the world, and what the runner owns once every effect goes through it.

for a principal

Weigh the boundary itself. Describing effects as values buys previewing, auditing and rewriting at the cost of expressiveness and of a runner your team maintains; say where in a system that trade lands well.

## Two things an effectful function can return A function that installs a package has two shapes available to it. It can **perform** the work and return whatever falls out — a status, a path, nothing at all — in which case calling the function *is* the installation. Or it can **describe** the work and return that description as a value, in which case calling the function produces a plan and installs nothing. The second shape is what "an action as a value" means. An action value is an ordinary value in the fullest sense: it can be bound to a name, stored in a collection, passed across a boundary, compared with another, thrown away. What distinguishes it from any other value is only its meaning — a runner that is given it will perform effects. ## Building the installer plan For an installer, the description might be three step cases assembled into a sequence: ``` function installPlan(name) return Sequence([ Fetch(name), Unpack(name), Link(name) ]) plan = installPlan("parser") // three step values exist; nothing downloaded ``` Everything interesting about the installer now happens in the builder, and the builder is a pure function from a name to a plan. The parts worth arguing about in review — which steps are needed, what order they take, whether an already-linked package should be skipped — are decisions about data. ## What the calling code gains | | performs the effects directly | returns an action value | |---|---|---| | what a call returns | a status, after the work is done | a description, with no work done | | what a test of the builder needs | a network, a disk, or fakes for both | a comparison of two values | | can the work be inspected first | no — it is already happening | yes — it is a value you can read | | can the work be rewritten | only by editing the builder | by transforming the plan | | when the effects happen | at the call site, one per call site | when a runner is handed the plan | Concretely, the gains are: - **A pure core to test.** Asserting that the plan for an already-unpacked package contains two steps rather than three is a value comparison; no fake network is involved. - **Inspection.** A second consumer can print the plan for an operator to approve, or count how many downloads it implies, because the plan is data rather than an event that already occurred. - **Rewriting.** Deduplicating a package that two plans both require, or reordering independent steps, becomes a transformation from plan to plan — checked the way any data transformation is checked. - **Composition.** Two plans concatenate into one plan. Two performed installations do not concatenate into anything; they have already happened. - **Honesty in the signature.** A function returning a plan announces that it is about effects, without having performed any. A reader can see where the effectful work is described and, separately, where it is run. ## Where the effects actually happen They happen in the runner, and nowhere else: 1. The builder assembles the plan — a pure computation. 2. Optional passes rewrite, merge or report on the plan — also pure, plan in, plan out. 3. One runner walks the final plan and performs each step. The program is not pure end to end; it is pure **up to the runner boundary**, and the boundary is a single named place rather than scattered through every function that happened to need a file. That is the whole architectural claim of the technique, and it is worth stating precisely, because candidates routinely overstate it into "the program has no side effects". ## What it does not buy you - **It does not remove the effects.** Somebody still downloads the archive; the runner does. - **It does not make the builder automatically pure.** If the builder itself reads the filesystem to decide which steps are needed, building the plan has effects, and the test that assumed otherwise will find the disk. Pass the facts in, or describe the reading as another step. - **It does not make execution repeatable.** Running the same plan twice performs its steps twice unless a step is designed to be repeatable. - **It costs expressiveness.** A description built from a fixed set of cases can only say what those cases can say; the moment a later step needs the result of an earlier one, the description needs a sequencing construct rather than a flat list, and that is a real design decision rather than a free upgrade.

  • The builder is supposed to be pure, yet its tests still touch the disk. What went wrong?
    The builder is deciding the steps by looking at the world — checking whether the package is already unpacked, say. That read is itself an effect. Either pass the facts in as arguments, so the builder is a function of data, or describe the check as another step in the plan and let the runner perform it.
  • What must an action value expose before a plan can be rewritten instead of merely deferred?
    Structure. A plan built from named cases can be walked, so a pass can drop, reorder or merge steps. A plan made of opaque callables can be stored and composed but never examined, so no pass can tell a fetch from a link. Inspectability is what rewriting needs.

A packing list is not the packing: you can copy it, cross out a line and hand it to someone else at no cost. Carrying the boxes is the part that cannot be copied or undone, and it happens once someone acts on the list.

saying these in an interview costs you the question

  • Says the fetch happens as soon as the plan is built
  • Claims returning a description makes the whole program pure
  • Thinks testing the builder still needs a real network
  • Believes building the plan twice performs the work twice
  • Says the only benefit is that the code reads more tidily