Stateful handlers: a parameter threaded across interceptions
A handler frame may carry a state value, given to each clause and replaced by
what the clause returns — a fold over the frame’s operation stream. This is
Plotkin–Pretnar’s parameter-passing handler (§6.5) restricted to ral’s
tail-resumptive fragment
(handlers-of-algebraic-effects),
where it needs no continuation: the clause shape is
(args, state) → (result, state′).
The gap
A ral handler clause is stateless between interceptions: bindings are immutable and the clause closes over a frozen environment, so nothing observable can change from one interception to the next. Inexpressible today:
- deterministic replay — record a session’s command outputs, then a handler that feeds them back one per interception (exarch agent tests);
- quotas — the first N
gitcalls pass, then fail; - counting / instrumentation without leaking through the filesystem or the environment.
Proposed semantics
- A handler entry may carry an initial state alongside its block; the frame owns the state for its dynamic extent.
- On interception the clause receives the operation’s arguments and the current state, and returns the substituted result and the next state — exactly once, in tail position. Resumption stays implicit; nothing is reified.
- Self-masking is untouched: the clause body runs with the frame stripped; only the state hand-off differs.
- The state is an ordinary
Value— it serialises with the handler stack like any shell state crossing a re-exec (transport). - The state dies with its frame:
withinexit discards it. This is dynamic state on the dynamic context, consistent with scoping.
Surface syntax is open (an entry shape like [init: V, step: { |args, st| … }]
versus a dedicated form); the semantics above is the proposal.
Non-goals
No first-class resume, no multi-shot, no Benton–Kennedy return clause — the
handler discipline is
unchanged. The clause remains a block; the only addition is the threaded
value.
Verify: a replay handler returns scripted outputs in order and fails when the script is exhausted; a quota handler admits N then fails; state visibly threads across ≥3 interceptions of one frame; the self-masking regression suite is unchanged; state is discarded on frame exit.
See also effects-handlers, handler-dispatch, handlers-of-algebraic-effects.