Algebraic effects and handlers
Handlers are the orthogonal layer that reinterprets an operation. That system calls are algebraic effects is the principle; whether to install a handler over an operation — and whether that handler reifies a continuation — are two independent choices layered on top, and ral takes the smallest on both: a restricted, continuation-free fragment.
ral installs effect handlers over a dynamic frame stack with
within [handlers: [name: handler], handler: fallback] { body }. A handler maps
an operation name to a lambda; the catch-all handler: applies when no per-name
entry matches. Handlers intercept external commands only — lexical bindings,
builtins, and the prelude are not shell aliases and cannot be overridden.
A per-name handler (and every alias) must be a unary lambda { |args| … },
applied to the intercepted command’s argument list; the catch-all must be a
binary lambda { |name args| … }, applied to the command’s name and its
argument list. The calling convention is fixed by the surface position, not the
value’s runtime shape: a bare block { … }, a non-lambda value, or a lambda of
the wrong arity is rejected at install time.
ral’s handlers are the tail-resumptive fragment of algebraic-effect handlers, along three axes — the first two recorded in handlers-deep-self-masking:
- Deep. A handler persists across the dynamic extent of the body, re-wrapping each continuation, so successive operations all reach it.
- Self-masking. While a handler’s own body runs, its frame is lifted from the stack, so a same-name call inside the body reaches the next outer handler (or the OS), never itself.
- Tail-resumptive. A handler receives the command name and arguments and either returns a value, forwards to the next enclosing frame, or fails. A returned value is substituted for the intercepted operation and the body resumes — that substitution is the resumption.
This is why there is no first-class resume: resumption is implicit,
exactly-once, and in tail position. What ral omits is the reified continuation
that non-tail or multi-shot resumption would require. The natural idiom is
wrap-and-forward, within [handlers: [git: { |args| my-git ...$args }]] { ... },
which self-masking keeps from diverging; the live semantic question is therefore the
masking discipline — which handler is in scope at each point — not how a captured
continuation is re-entered.
See also syscalls-are-effects, control-operators, scoping, grant, system-c (self-masking as capability-set subtraction), handlers-of-algebraic-effects (the calculus this fragment restricts).
Realised in handler-dispatch.
Cite: RATIONALE §“Effect handlers: deep with self-masking”; docs/SPEC.md §3.2.