Features · Reversible

Reversible

Not only is Program reactive in the forward direction, from state to outputs, but also in the reverse direction: if an output is updated, the state is updated accordingly.

Two-way, without the wiring

This is the part you normally have to wire up yourself, with some kind of state-management pattern. With reversibility it comes for free, so you can always work locally on any function or feature and it will plug seamlessly into the overall state of the application, or any local state.

STATE Maths.Real celsius = 0.0
LET Maths.Real scaled = Maths.Real.Multiply celsius 1.8
LET Maths.Real fahrenheit = Maths.Real.Add scaled 32.0

REVERSE Maths.Real fahrenheit
  LET Maths.Real offset = Maths.Real.Subtract fahrenheit 32.0
  SET celsius = Maths.Real.Divide offset 1.8

celsius drives fahrenheit forwards; the REVERSE block runs the same maths backwards, so writing fahrenheit updates celsius too, and SET pushes the result into outer state.

The shape of it

Forwards, an output is a pure function of state — the same idea as reactivity. Reversibility adds the other direction: writing an output is a pure function back to the state it came from. You describe the forward calculation once; the reverse runs it backwards, so a control bound to a value can change it and the change lands where the value was read.

state celsius calculation ×1.8 +32 output fahrenheit reverse — write the output, state follows
Forwards, state becomes the output. Writing the output runs the calculation in reverse, back to state.

No event handlers

This is what reversibility is really for. In most frameworks an interaction deep in the interface is reported back up by hand: each layer takes callback props — onComplete, onDelete — and passes them to the next, an ever-growing tree of handlers that dominates the code and is hard to test. With two-way bindings that tree disappears. A component is handed its data as a BINDING and writes to it directly; the write finds its own way home.

FUNCTION Ui.Content TaskRow
  INPUT BINDING Task task
  LET Logic.Boolean done = Record.Field task "done"
  REVERSE Logic.Boolean done
    SET task = Record.WithField task "done" done
  Ui.Input.Checkbox done
  Ui.Content.Text (Record.Field task "name")

The row takes a task as a two-way BINDING, not a stack of callbacks. Ticking the box writes done; the REVERSE writes the updated task back; and because the task is itself a binding, that update keeps travelling outward until it reaches real state. The parent passed data down and got the change back for nothing.

Working out the reverse

For a simple calculation the reverse is obvious and Program supplies it for you — double a value and writing the result halves it. Where it is ambiguous you give a hint; where it is genuinely hard you write it yourself. Adding values into a total is the classic ambiguous case: if the total is edited, which input gives way?

STATE Maths.Real groceries = 12.00
STATE Maths.Real pettyCash = 0.00
LET Maths.Real total = Maths.Real.Add groceries pettyCash

REVERSE Maths.Real total
  SET pettyCash = Maths.Real.Subtract total groceries

The hint is just this: keep the known figure, let pettyCash absorb the difference. Editing total changes one value, exactly as intended. You only ever spell out a reverse when the meaning is yours to decide.

Reversible by construction

Some reverses are so common they are built in, so you rarely write them. Reading a field out of a record binding hands back a binding: write it, and the record updates with that one field changed. The same holds for an element of an array. Most interface code is just this — take a piece of data in, fetch parts of it out for sub-components, and edits flow back through those fetches without a line of write-back code.

A deliberately small problem

Reversibility here is not the general, hard kind. Every interaction in an interface already maps to a definite change in the underlying data — that is what makes it interactive — and that change is usually small and direct. Program leans on that fact rather than fighting it.

It is not a solver: you will not type a hash and have it un-hashed, or a number and have it factorised. For the ordinary filtering, formatting and field edits an interface is made of, the reverse is either obvious or a line or two — and that covers almost everything.