Chapter 12 · Part V

Propagators: Computing with Partial Knowledge

Some answers do not reduce from inputs to output. They accumulate — from many directions, in any order, out of fragments — and settle into place. This is computation as a network of partial knowledge, kept reliable by invariants.

Every technique so far, however open, has computed in one direction. A workflow flows a draft to a shipment; an evaluator pulls a price from the leaves of an expression to its root. Inputs go in; an output comes out; the direction is fixed. Part V is about problems that refuse that shape — where knowledge arrives in fragments, from several directions at once, and the answer must be assembled rather than reduced.

The quoting problem in the running system is exactly this. What an order finally costs is constrained from many independent directions: inventory bounds what can be fulfilled, a promotion lowers the ceiling, the order's floor raises the base, tax jurisdiction shifts the total, shipping adds to it. None of these is the input; each is a partial fact about the same unknown quote, and they may arrive in any order, some late, some never. A function cannot model this, because a function must know its inputs before it runs. What models it is a propagator network.

12.1 Computation That Isn't a Function

You already rely on propagator networks daily, under other names. A spreadsheet is one: cells hold values, formulas connect them, and changing one cell wakes every formula that depends on it, which updates its cell, which wakes the next — computation spreading through a network until it settles. Reactive UI signals are the same idea: a derived value recomputes when any source changes. A constraint layout engine, like the one behind Auto Layout, solves position and size from constraints that arrive from all sides. An incremental build system repropagates only what a change can reach. Each is a special case of one general model: cells that hold information, and machines that keep the cells consistent as information arrives.

The generalization, due to Radul and Sussman, makes two moves past the spreadsheet. First, a cell holds not a value but partial knowledge — it may know nothing yet, or a range, or an exact value, or that it has been told contradictory things. Second, the connections are multi-directional: where a spreadsheet formula computes one cell from others, a propagator can run whichever way the available knowledge allows. Together these let a network compute with what it has, from wherever it comes, in whatever order it arrives.

12.2 Cells and Partial Knowledge

The heart of the model is the content of a cell, and the rule for combining two pieces of it. For the quote, partial knowledge is a range: the price is known to lie somewhere between a low and a high bound. Knowing nothing is a wide range; knowing exactly is a range collapsed to a point.

-- Partial knowledge about a price: it lies somewhere in [low, high].
class Price_Range
  create make(l: Money, h: Money)
    require
      ordered: l <= h              -- an empty range is a contradiction; see below
    do
      low := l  high := h
    end
  feature
    low: Money
    high: Money

    -- Merge two pieces of knowledge into a sharper one: intersect the ranges.
    merged_with(other: Price_Range): Price_Range
      do
        let l: Money := low
        if other.low > l then l := other.low end
        let h: Money := high
        if other.high < h then h := other.high end
        result := create Price_Range.make(l, h)
      end

    is_known(): Boolean do result := low = high end
  invariant
    ordered: low <= high
end

The one operation that matters is merged_with, and it has a property that makes the whole model work: merging only ever produces knowledge at least as sharp as what went in. Intersecting ranges can narrow the bounds or leave them, but never widen them. Information in this world is monotone — it accumulates and never retreats. That single discipline is what frees the network from caring about order: because merging is commutative and associative and only ever sharpens, the cell reaches the same final content no matter which fact arrives first, or how many times. Order-independence is not engineered; it falls out of a well-chosen merge.

12.3 The Machines Between the Cells

A cell holds knowledge; a propagator moves it. A propagator is a stateless machine wired to some cells: when any of them gains sharper content, the propagator wakes, computes what that implies for the others, and adds it — which may wake further propagators, and so the sharpening spreads until nothing new can be derived and the network goes quiet.

class Cell
  create make(initial: Price_Range) do content := initial  neighbors := [] end
  feature
    content: Price_Range
    neighbors: Array[Propagator]

    -- Add a fact from any source; keep only if it sharpens; then wake the network.
    add_content(info: Price_Range)
      do
        let sharper: Price_Range := content.merged_with(info)
        if not (sharper.low = content.low and sharper.high = content.high) then
          content := sharper
          across neighbors as p do
            p.wake
          end
        end
      ensure
        never_widens: content.low >= old content.low and content.high <= old content.high
      end
end

The postcondition never_widens states the monotonicity in the type of a contract: a cell's knowledge can only get sharper. That guarantee is what makes a propagator network something you can reason about at all — without it, a cell could oscillate and the network might never settle. Because propagators are stateless and only add information, they are also multi-directional: a propagator relating a subtotal, a discount, and a total can fire in whichever direction it has the inputs for — deriving the total from the first two, or bounding the discount once the total is known. There is no privileged input and output, only cells that know more or less.

12.4 The Quoting Network

Now the running example resolves. The quote is a cell, initialized knowing almost nothing — a range from zero to some ceiling. Each source is a propagator that adds what it knows, from its own direction, whenever it learns it:

let quote: Cell := create Cell.make(create Price_Range.make(Money.zero, ceiling))

-- Each source contributes a partial fact; order does not matter.
quote.add_content(floor_bounds)       -- the order floor raises the low end
quote.add_content(promo_bounds)       -- a promotion lowers the high end
quote.add_content(tax_bounds)         -- jurisdiction shifts the range
quote.add_content(inventory_bounds)   -- limited stock caps the fulfillable amount

if quote.content.is_known then
  -- low = high: the sources have pinned the quote to a single price
end

This is additive design carried to its limit. A new source of pricing knowledge — a seasonal surcharge, a regional subsidy — is a new propagator wired to the quote cell. It is never an edit to the others; the network simply has one more way to learn. And because merging is monotone and order-free, it does not matter whether the surcharge is known at checkout or arrives a second later from a slow service: the cell integrates it whenever it comes and settles to the same answer. The engine computes with partial knowledge and keeps computing as knowledge fills in.

12.5 Invariants Keep the Network Reliable

An open, multi-directional network is a frightening thing to trust. There is no single evaluation order to trace, no function signature that captures what it computes, and any number of independently-authored sources feeding it at once. What stops two of those sources from quietly disagreeing — a promotion insisting the price is at most ten, a floor insisting it is at least twelve — and the network settling on a confident, wrong number?

The invariant does. Look again at Price_Range: its ordered invariant, and the matching precondition on make, say that the low bound may never exceed the high. When two sources disagree, their merge tries to construct a range whose low is above its high — and the contract fires, at the exact moment and place the contradiction entered, naming it. A silent wrong answer becomes a located, explained failure. In a full system this is the signal a truth-maintenance layer uses to back out an assumption and try another; here the point is narrower and it is the point of the whole book: in a network too open for the type system to check, the invariant is what catches the inconsistency. Constraint does not disappear at the most flexible edge of the system; it moves into the merge, where it guards a computation no signature could describe.

A propagator network is the most open computation in the book — no fixed direction, no fixed inputs, no fixed set of contributors. Its reliability rests entirely on one invariant on the merge: information may sharpen but never contradict. Where flexibility is total, a single well-placed constraint is what keeps it from lying.

12.6 Provenance, and Taking Facts Back

Monotonicity buys order-independence but seems to forbid something a real quoting engine needs: a promotion can be withdrawn, and its contribution to the quote must then disappear. Information that only accumulates cannot simply be un-accumulated.

The resolution is to record not just what a cell knows but why — the provenance of each contribution, the set of premises it rests on. A cell's content becomes knowledge-relative-to-assumptions, and retracting a premise means recomputing the cell from the contributions that did not depend on it. Within any fixed set of assumptions, information is still monotone and the network still settles; across a change of assumptions, a supervising layer — a truth-maintenance system — rebuilds what the withdrawn premise supported. The withdrawn promo cleanly retracts, not by reversing time, but by asking what the network would have known without it.

Propagators show flexibility in a new dimension — not an open vocabulary, as in Part IV, but an open topology: any number of sources, in any direction, over time. There is one more shape of partial-knowledge computation to meet, and it is the most open technique in the book. Instead of narrowing numeric ranges, it matches and merges structures — asking not "what is the price?" but "what assignment of unknowns makes these shapes agree?", and searching when more than one might. That is unification and search, and it is the final chapter of the frontier.