Chapters

Reactivity in Catena

Most of a sequence runs the way you read it: top to bottom, once. Line one runs, then line two, and when the last line finishes the sequence is done. Reactivity breaks that habit for the lines that need it. A reactive line is not a step in the sequence — it is a standing rule that watches one or more values and responds whenever they change. You write the rule once; the calculator decides when to run it, and runs it as often as the watched values move.

This is the spreadsheet idea you already know. In a spreadsheet, a cell that reads =A1*1.2 does not run "in order"; it simply stays correct — change A1 and the cell updates itself. Catena brings that same live-recompute behaviour into a sequence, but with two deliberate safeguards spreadsheets lack: reactive lines are written differently from ordinary steps, so you can always see which lines are rules, and every rule states plainly when it is allowed to fire.

If you have not yet read how ordinary sequences run, start with Sequences and functions; this chapter assumes you know how a plain assignment like amount × 1.05 -> amount works.


What "reacts" and what does not

A normal statement is an event: it happens at one moment, in sequence order, and then it is finished. A reactive rule is a relationship: it declares that some value should always follow from others, and it re-runs itself to keep that true.

The keyword for reactivity is WHEN. It introduces a rule with a condition and a body. The condition names the values the rule watches; the body is what runs when the rule fires. A rule always closes with END:

WHEN amount > 5000 ALWAYS
    amount × 1.05 -> amount
END

Read that as a standing instruction, not a step: "whenever amount is over 5000, keep amount scaled by 1.05." The rule watches amount. It is not something the sequence "reaches" and executes once — it is armed the moment it is declared and fires on its own terms afterward.

Two things decide those terms, and every WHEN must pick one of them: ALWAYS (fire eagerly, the moment a watched value changes) or AS RULE (fire only when you ask). They are covered in turn below.


The two reactive forms

Form When it runs Use it for
WHEN … ALWAYS Eagerly — every time a value it depends on changes A live value: a "cell" that is always current
WHEN … AS RULE r Deferred — only when triggered by CHECK r A check or adjustment you apply once, after a value has settled

Both forms share the same shape: WHEN, a condition, the timing (ALWAYS or AS RULE r), then the body and END. The only difference is when the body is allowed to run. That difference is the whole point of the feature, so Catena forces you to state it — see Why there is no bare WHEN below.


The eager form: WHEN … ALWAYS

ALWAYS is live recompute. The rule fires immediately, every time any value in its condition or body changes. You never call it; it keeps itself current in the background, exactly like a spreadsheet cell.

Use it for a value that should always be up to date the instant its inputs move — a derived total, a running conversion, a status that mirrors some other figure.

Worked example — a derived total that keeps itself current. Suppose a sequence tracks a net figure and you want a gross figure that always carries 20% tax on top:

WHEN net ALWAYS
    net × 1.20 -> gross
END

100 -> net        # gross becomes 120
250 -> net        # gross becomes 300

You define the relationship once. Each time net is assigned, the rule re-runs on its own and gross follows — 120, then 300 — without you writing the multiplication again. If ten later lines all change net, gross is correct after every one of them, because the rule fires after each change. That is what "eager" means: no waiting, no asking.

The condition can also gate the rule. In the earlier WHEN amount > 5000 ALWAYS example, the body runs eagerly, but only while the condition amount > 5000 holds; when amount is 5000 or less the rule watches quietly and does nothing.


The deferred form: WHEN … AS RULE with CHECK

Eager recompute is not always what you want. Sometimes a value is being built up over several lines, and you do not want a rule reacting to every half-finished intermediate state — you want it to run once, at the end, on the final value. That is what AS RULE is for.

AS RULE r gives the rule a name (r) and defers it. Declaring the rule arms it but does not run it. It runs only when you explicitly trigger it with CHECK:

CHECK surcharge        # evaluate the rule named "surcharge" now
CHECK                  # evaluate every settled rule now

CHECK r evaluates the one named rule. A bare CHECK (no name) evaluates all of the deferred rules that are ready. The name is how you connect the two halves: the AS RULE surcharge declaration and the CHECK surcharge trigger refer to the same rule.

Worked example — an adjustment applied once, after the value settles. A price is assembled from several parts, and a 5% surcharge should apply to large orders — but only to the final assembled amount, not to each partial sum along the way:

WHEN amount > 5000 AS RULE surcharge
    amount × 1.05 -> amount
END

0 -> amount
base -> amount            # add the base
amount + shipping -> amount   # add shipping
amount + handling -> amount   # add handling
CHECK surcharge           # now, on the finished amount, apply the rule

While amount is being built up line by line, the surcharge rule stays dormant no matter how the figure crosses and re-crosses 5000. Only at CHECK surcharge, on the settled total, is the rule evaluated and the surcharge applied — exactly once. This is the deliberate answer to the "accidental trigger while a value is still being built up" problem: you finish computing, then check.

Use AS RULE for validations and adjustments you want to apply at a controlled moment: a final constraint on a result, a one-time correction, a rule you run again later by naming it in another CHECK.


Why there is no bare WHEN

You cannot write WHEN on its own. Every WHEN must be qualified with either ALWAYS or AS RULE; a bare WHEN is rejected before the sequence runs, with a validation message telling you to add the timing.

This is on purpose. The single most important fact about a reactive line is when it fires — immediately, or only on CHECK. Forcing that word to appear at the rule itself means no reader ever has to guess whether a line runs eagerly or stands waiting for a trigger. The timing is stated where the rule is written, every time.


WHEN is a rule, not a decision

WHEN is easy to mistake for IF, and Catena keeps them strictly apart. IF is ordinary control flow: it is reached in sequence order, tests its condition once at that moment, runs the matching branch, and is finished — a one-time decision. WHEN is a standing reactive rule that keeps responding to its inputs over the life of the sequence.

Because the two describe different evaluation models — "decide once, here" versus "keep this true" — WHEN is never a synonym for IF. If you want a single branch at one point in the flow, use IF. If you want a value or check that maintains itself as inputs change, use WHEN.


Where a rule can see: scope

A reactive rule is local to the sequence that declares it. It watches that sequence's own variables, and it is invisible to any other sequence — including sequences reached with CALL. This follows from how sequences work: each one is a self-contained unit that shares nothing but its parameters (in) and its returned values (out). Because the variables a rule watches are local, the rule is local too.

In practice this means you never have to worry that a rule you wrote here will fire on a value inside a sequence you call, or that a called sequence's rules will reach back into yours. Each sequence's reactive rules belong to it alone. For how sequences pass data in and out, see Sequences and functions.


Typical uses

  • A running validation. Declare WHEN result < 0 AS RULE nonNegative and CHECK nonNegative after you finish computing result, to flag an impossible value once the figure has settled — a machine-checkable statement of what the answer is allowed to be.
  • A derived total that keeps itself up to date. Declare WHEN subtotal ALWAYS with a body that recomputes a tax-inclusive total, so the total is always correct no matter how many later lines revise the subtotal.
  • A staged adjustment. Build a figure across several steps, then CHECK a named AS RULE once at the end so a surcharge, rounding, or correction applies exactly once to the finished value rather than to every intermediate.

When you are unsure which form to reach for, ask a single question: should this run the instant its inputs move (ALWAYS), or once at a moment I choose (AS RULE plus CHECK)? That answer is the whole decision.


See also