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.
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.