Effects and handlers¶
An effect declaration gives names and types to requests. An instance gives one
application of that interface a lexical identity. A computation's effect row
then records the instances it may request from, and a handler interprets one
instance. This separation is the lexical-instance discipline (LID): two
instances of State[Int] have the same interface but remain distinct effects.
Declare an interface¶
effect State[S : Type]
get : Unit -> S
put : S -> Unit
instance left : State[Int]
instance right : State[Int]
define exchange(next : Int) : Int !{left, right} =
let previous <- perform left.get()
perform right.put(next)
return previous
Square brackets bind static parameters on the interface or an operation. An
operation's argument types precede ->; an operation with no arguments may
write either Unit -> T or the corresponding nullary form admitted by the
grammar. A request is always qualified by an instance:
perform left.get(). This qualification is what keeps left and right
separate in the row.
Prelude interfaces¶
These interfaces are available without a source declaration:
| Interface | Principal operation | Purpose |
|---|---|---|
Random |
sample[A](Site[A], Sampleable[A]) -> A |
draw or replay a named random value |
Score |
add(LogWeight) -> Unit |
add a density or factor |
State[S] |
get, put |
lexical mutable state under a handler |
Abort[E] |
abort[A](E) -> A |
typed early exit |
Choose |
choose[A](Tensor[A]) -> A |
nondeterministic search |
Weight[K] |
add(K) -> Unit |
semiring-parametric accumulation |
Compute[X, A] |
apply(X) -> A |
process-local host computation |
Param |
get[A](String) -> A |
named learned-parameter lookup |
Search is generated for a deduction or schema-backed parser because its
choice arity depends on the elaborated system. Structural declarations create
Compute instances; a caller normally encounters them through
Program.run, not by writing the requests manually.
Effect rows¶
A computation signature ends in an effect row:
| Row | Meaning |
|---|---|
!{} |
closed and pure |
!{left, right} |
exactly these lexical instances |
!{robust | rho} |
robust plus an open tail rho |
!{robust | rho lacks robust} |
open tail that cannot already contain robust |
Rows contain instances, not interface names. A call contributes its callee's
row after static substitution. A perform contributes the named instance. An
if or case joins the rows of every reachable branch. Conservative case
coverage also means that an unproved-impossible branch contributes its
effects.
lacks constraints prevent accidental duplication when a row-polymorphic
computation adds an instance of its own. Use them when an abstraction handles
or introduces the same lexical name that a caller's tail might otherwise
contain.
Handler declarations¶
A handler declares the interface it handles, an input and answer type, options, one return clause, and operation clauses:
effect Robust
shrink : Real -> Real
instance robust : Robust
handler half_weight for Robust : Real -> Real [coverage=total, forwards=none, implementation=authored]
return x =>
return x
shrink(x : Real) resumes 1 =>
resume(0.5 * x)
define robustify(x : Real) : Real !{} =
handle robust with half_weight in
let adjusted <- perform robust.shrink(x)
return adjusted
The return clause handles a computation that finishes without another
request. Each operation clause receives the request's fields and may call
resume. Handlers are deep: a resumed continuation runs beneath the same
handler. Effects performed by the clause body itself are offered only to
outer handlers.
Handler options¶
| Option | Values | Effect |
|---|---|---|
coverage |
total, partial |
whether every operation is handled |
forwards |
none, unknown |
whether a partial handler passes uncovered requests outward |
implementation |
authored, foreign |
whether clauses have QVR bodies or runtime attachments |
introduces |
instance names | additional effects a handler body may perform |
A total handler removes the matched instance from the body's row. A partial handler retains it. Forwarding and coverage are separate: an uncovered operation in a non-forwarding partial handler is an error rather than an implicit outer request.
Resumption grades¶
| Grade | Contract |
|---|---|
0 |
the clause must not resume |
aff |
at most once |
1 |
exactly once |
omega |
any number of times when the continuation is duplicable |
The checker records the grade, and the evaluator enforces it. omega is used
by search handlers that explore several alternatives; a state update that
continues once normally uses 1; an abort clause uses 0.
Scoped instances¶
with instance creates a fresh lexical identity inside a computation:
effect State[S : Type]
get : Unit -> S
put : S -> Unit
handler run_state for State[Int] : Int -> Int [coverage=total, forwards=none, implementation=authored]
return x =>
return x
get resumes 1 =>
resume(7)
put(s : Int) resumes 1 =>
resume(unit)
define local_read() : Int !{} =
with instance cell : State[Int] in
handle cell with run_state in
perform cell.put(3)
let value <- perform cell.get()
return value
The instance does not exist outside its block. A total handler must eliminate
it before control leaves; otherwise the checker reports
qiec-instance-escape. Nesting two with instance cell ... blocks creates two
identities even though the source spelling is shadowed.
Foreign handlers and providers¶
A clause with no => body is a signature for a process-local implementation,
and the declaration must say implementation=foreign. The stable module
contains the handler type, operation coverage, and grades, but no callable.
At invocation, the selected runtime provider binds the handler's stable ID to
an implementation. Runtime configuration files contain provider names and
data only; they cannot inject code.
The built-in core provider supplies the reference implementations used by
program execution, deductions, and the standard prelude. Structural models
compose core with a structural provider holding their compiled PyTorch
modules. See Execution and tooling.
Common failures¶
- Request absent from the row. Add the lexical instance or make the row polymorphic when the abstraction is intended to preserve caller effects.
- Wrong applied interface.
State[Int]andState[String]are distinct; a handler must match the instance's exact application. - Instance escape. Handle a scoped instance before leaving its block.
- Grade violation. Make the number of
resumecalls match the declared grade. - Missing provider. Attach a provider that implements the foreign handler, or use an authored handler.
The effects tutorial builds and traces an authored handler end to end.