Chapter 7. The Write Side

Definition 1.1 has a second component, and everything derived so far concerned the first. read was the easy half: documents can be declarative, because a page that only displays needs no program. Applications also change things, and change is widely held to be where declarative architectures fail. This chapter derives the write side in four propositions. Nothing from the read pipeline needs to be taken back; the write side is the factorization’s mirror image, and the smaller of the two.

The delta normal form

Start where Chapter 5 left the state: State = 𝒫(Fact), merge is union. What can change about a set? Elements are removed and elements are added. There is no third possibility.

Prop. 7.1 (Delta normal form). Every state change factors as a pair of fact-sets, a delta:

write(r, S) = (S ∖ D⁻) ∪ D⁺                              (7.1)
D⁻ = S ∖ write(r, S)          the facts removed
D⁺ = write(r, S) ∖ S          the facts added
Proof — extensionality, then minimality.

With D⁻, D⁺ as defined, (S ∖ D⁻) ∪ D⁺ = write(r, S) by set extensionality. Minimality: any pair (A, B) with (S ∖ A) ∪ B = write(r, S) satisfies A ⊇ D⁻ (a fact of S absent from the result leaves only by removal) and B ⊇ D⁺ (a fact new in the result enters only by addition). So (D⁻, D⁺) is contained in every such pair: it is the least pair, and a least element is unique. ∎

A state change is two sets. That is the entire theory of mutation over a fact-set model. Measure that against what the industry maintains for mutation: object-relational mappers, undo stacks, reconciliation engines. Every one of these is machinery for computing or applying change over a model in which change has no normal form. Trees are the instructive case: two trees have no canonical difference, so deciding what “changed” is a heuristic. The industry’s clearest specimen is the virtual DOM’s diffing engine (Chapter 11): client-side frameworks re-compare an in-memory copy of the page on every render. That is a whole runtime recovering, approximately, what (7.1) gives exactly by subtraction. Mutation’s normal form is therefore a by-product of the model R2 forced for merging, because union and difference belong to one algebra.

On the running example: the wind gusts, and the panel’s value moves. The delta is D⁻ = {(⟨…#panel-14⟩, value, "15.5 kW")} and D⁺ = {(⟨…#panel-14⟩, value, "16.1 kW")}, two one-element sets. That is the entire update, transport included.

S — the State
(⟨…#panel-14⟩, title, "Current Power")
(⟨…#panel-14⟩, value, "15.5 kW")
(⟨…#panel-7⟩, title, "Wind Speed")
(⟨…#panel-7⟩, value, "8.2 m/s")
D⁻ — the facts removed
panel-14 value "15.5 kW"
D⁺ — the facts added
panel-14 value "16.1 kW"
the wind gusts; the delta is two one-element sets.

Interactive exhibit (online edition): the gust, applied. Edit the two sets and apply them against the live state. Apply the same delta twice and watch nothing happen: sets subtract, and re-application is a no-op.

A delta is made of fact-sets. Change is data in the same model as the state it changes. There is no second model, and no change-description language whose semantics would have to be invented. The delta is what fills the request’s Body from Chapter 1, and Chapter 8 will give the industry’s name for it.

Forms, run backwards

Prop. 7.2 (Forms are inverse transforms). The read pipeline ends at a human; the write side begins at one. The instrument is a form: a tree, part of a document, rendered by present like everything else. Its fields are a fact pattern’s variables. A form carries one pattern or several, each marked “remove” or “add”; submission binds their fields, and a bound pattern is a set of facts. The marked sets are (7.1)’s delta:

form   : Tree                fields ↔ variables of a fact pattern
submit : Bindings → (D⁻, D⁺)                             (7.2)

On the running example: the edit form arrives holding “15.5 kW”; the user types “16.1”; the stale binding, marked remove, is D⁻, and the fresh binding, marked add, is D⁺. One submission produces both sets.

A form is arrange run backwards. The one factor that crossed graph→tree (Chapter 6) is also the one that must cross back, and it crosses back using patterns, exactly as before.

Transforms are known to be hard to run backwards. Databases call it view update: given a change to a view, find the change to the data underneath that produces it. That change need not exist, and need not be unique. The programming-language answer is lenses, a forward transform paired with a backward one that must satisfy round-trip laws. The ambiguity does not arise here, because the inverse is declared rather than computed. A form’s fields are a pattern’s variables, so the backward direction is fixed when the pattern is written. The restriction comes from S2: the read side is terms of a fixed algebra, never arbitrary functions. Appendix D carries the citations.

A form does two jobs, and they are different in kind. Construction decides which fields an edit form offers for an entity of this kind; it is a projection of structure: read the patterns, render inputs. Validation decides which deltas are admissible; it is a predicate on (D⁻, D⁺). A schema drafted to do both jobs at once will do both badly. Chapter 20 builds on that point.

For the justification, view source on any HTML form since 1993. Field names are attribute names; method names the unsafe verb; the form is a fact pattern whose variables are input boxes. The web has shipped the inverse transform beside the forward one from the beginning.

One algebra, both directions

Prop. 7.3 (One algebra, both directions). Chapter 5’s selection algebra (match, join, union, project) is the write side’s algebra too. A pattern with free variables selects the facts that match; the same pattern with its variables bound denotes the facts of a delta.

pattern + state      →  bindings                          (find)
pattern + bindings   →  (D⁻, D⁺)                          (change)

The consequence is practical as much as formal: the write side adds no expressive machinery. Whoever can query can update: an implementation gets its update language by handing its pattern matcher the bindings it would otherwise return. When Part III proves the read side complete, the write side inherits the result through this symmetry. Compare, once more, the industry’s arrangement. It uses a query language, a separate mutation API, a migration DSL, and a client-side state manager. That is four vocabularies for one algebra.

The five moves

Prop. 7.4 (Interactivity, decomposed). The objection’s strongest form: “real applications are interactive.” By independent evolution (Prop. 4.5) the document is doc(r, τ) = read_τ(r, S(τ)). That is a value with exactly five inputs: the request r, the state S(τ), and the three factor terms. So every interaction ever shipped on the web is one of exactly five moves:

  1. navigate — a new r: link, filter, page, search. The term stays the same; the argument changes.
  2. write — S advances by a delta (7.1): submit, edit, delete.
  3. restyle — substitute the present term: the theme toggle.
  4. rearrange — substitute the arrange term: list to grid, sort, collapse.
  5. reselect — substitute the select term: a saved query edited, a dashboard reconfigured.

Moves 2–5 are independent evolution’s four timelines; move 1 is the request, an argument to the application, not a component of it (Chapter 4). There is no sixth move because there is no sixth input. Interactivity is the factorization being exercised. Fusion adds no move to this list; what it gains is the freedom to make the moves without saying which component they touch. Part IV will show what that freedom costs.

The latency concession

One concession remains, latency, and it too should be met at full strength. The operational complaint is the round trip: a keystroke should not cross an ocean to move a cursor. Granted. But look at what the complaint actually asks for: that the factors be evaluated near the user, not that they be fused. And mobility of evaluation is precisely what S2 already secured. A term whose semantics is closed evaluates the same everywhere. So ship q, t, s to the client and run them there, against a local replica of the selected data. No proposition of the architecture has changed. The terms and the factors are the same; only the machine is different. What cannot travel this way is a fused read: an opaque program can only be shipped whole and trusted blind. Shipping opaque programs to browsers is an experiment the web has already run; Chapter 12 reports the result. Declarative terms are portable because they mean the same thing everywhere. That is S2, applied as a deployment strategy.

The closed pipeline

The pipeline is now complete in both directions, and it closes:

The closed pipeline. Three factors turn state into a document, a human reads that document, the human’s answer is a delta, and that delta becomes the next state as τ ticks (4.5). Every arrow but the human’s is a numbered proposition. Everything before this figure derives it; everything after measures the world against it.

Change, the objection said, is where declarative architectures fail. In the diagram, change is the arrow that closes the pipeline.