Effects
Partial — perform,
handle, inferred latent-effect unions, higher-order propagation, recursion,
and module-interface round trips work today. Applying an unhandled effectful
function is E0202. Handlers are shallow in-scope dispatch (resume is an
identity continuation, not a captured continuation), and standalone effect
declarations are not implemented yet.
Algebraic effects separate what a computation requests from how the request
is served. A function says perform Log.log msg; the caller decides whether
that means a file append, a network call, or nothing at all. The same function
works in production, in tests, and in a REPL without changing a line.
perform and handle
Section titled “perform and handle”perform Effect.op arg requests operation Effect.op from the nearest
enclosing handle that covers it. A handler clause receives the operation’s
arguments plus a resume continuation; calling resume value returns value
to the perform site and continues the handled expression:
handle let x = perform State.get () in x + 1with State.get () resume -> resume 41 return val -> valend# => 42Reading the example: the body performs State.get (). The handler’s State.get
clause answers with resume 41, so the perform expression evaluates to 41,
the body continues, and produces 42. The return clause then transforms the
body’s normal result — here it is the identity.
The general shape is:
handle <body> with Effect.op args resume -> <clause body> ... return val -> <return body>end- Operations are identified by their
Effect.oplabel at theperformsite. There is no separate declaration step today —effect ... enddeclaration blocks are planned but do not parse yet. - The
return val -> …clause runs on the body’s normal result (not on eachresume). It is optional and defaults to identity. - Nested handlers shadow outer ones for the operations they cover: the innermost
covering
handlewins.
handle handle perform State.get () with State.get () resume -> resume 99 return val -> val endwith State.get () resume -> resume 0 return val -> valend# => 99 (the inner handler answers)Effect rows on function types
Section titled “Effect rows on function types”Function arrows carry a latent effect expression: the operations the
function may perform plus any open sources. The compiler infers it from
perform and applications. You never write it in source; diagnostics show its
normalized summary as !{Effect.op} (or !{|r} when it remains open):
\x -> perform State.get () # a -> !{State.get} b\f x -> f x # propagates f's (possibly open) effectWhat is implemented today:
performinside a lambda, when no coveringhandleis in scope, is recorded on that function’s row.- Application unions every callee source into the enclosing function —
let apply = \f x -> f xandlet g = \() -> f ()propagate effects without equating independent callbacks. handlesubtracts the operations its clauses cover. Ops not covered escape into the enclosing row.handlecovers apply. Applying the function inside a handler for those operations is accepted — includinglet f = \x -> perform E.op x in handle f v with ….- Application of an uncovered row is E0202 (below).
- Direct
performwith no enclosing lambda and no handler stays a-Wunhandled-effectwarning.
Pure direct and mutual recursive components use a least-derived summary;
higher-order and imported opaque sources remain open. .yonai files carry
effectscheme, a deterministic normalized description of every arrow, shared
open source, and mask. Import clones it with fresh variables.
E0202 — unhandled effects are errors
Section titled “E0202 — unhandled effects are errors”Applying a function whose row is not fully covered by any surrounding handler is
a compile-time error (E0202), not a warning. The primary diagnostic points
at the introducing perform, with a note at the application that let the
effect escape. Curried partial applications are pure; the final source
application reports each escaping operation once:
let f = \x -> perform State.get () in # f : a -> !{State.get} Intf 0# error[E0202]: unhandled effect State.get# --> points at `perform State.get ()`# note: applied here with no covering handlerThe fix is a handler at the use site:
let f = \x -> perform State.get () inhandle f 0 with State.get () resume -> resume 7 return val -> valend# => 7A direct perform with no handler in scope (not mediated through a function
application) compiles with a -Wunhandled-effect warning and raises
:UnhandledEffect at runtime if reached.
Rows cross module boundaries
Section titled “Rows cross module boundaries”.yonai FN lines retain a readable closed summary:
FN YonaTestFxFetch 1 STRING -> STRING effects Fs.readInterfaces also append effectscheme, which preserves all arrow positions,
independent open variables, and handler masks. That lets helpers such as
use f g n = (f n, g n) cross a module boundary without losing either callback
effect or making sibling instantiations share state. A missing effects field
stays unknown, never pure.
Worked example: GPU fallback
Section titled “Worked example: GPU fallback”Std\Gpu uses effects to let you decide what a GPU failure means. Kernels
report issues as ordinary values; raiseGpu converts an issue into a
perform Gpu.*, designed to be answered by a handler at your call site:
import raiseGpu, GpuOom from Std\Gpu inhandle raiseGpu GpuOom # performs Gpu.oom ()with Gpu.oom () resume -> resume () # e.g. log and fall back to CPU Gpu.deviceLost () resume -> resume () Gpu.fail code resume -> resume () return val -> 1end# => 1Step by step: raiseGpu GpuOom performs Gpu.oom (); the handler resumes with
(), so the handled expression yields (); the return clause maps that to
1. A different caller could resume differently — retry, abort, or switch
backends — without touching the kernel code. withGpuFallback action wraps this
pattern: it runs action, then performs the matching Gpu.* operation for the
last classified issue (no-op on success).
Current limitations
Section titled “Current limitations”Stated plainly, because they shape what you can write today:
- Handlers are shallow, in-scope dispatch.
resumeis an identity continuation: the clause computes a value and execution continues at theperformsite. You cannot captureresume, call it later, call it twice, or decline to call it to abort with a different value — patterns like backtracking, generators-via-effects, and resumable exceptions that need a first-class delimited continuation are not expressible yet. effectdeclarations do not parse. Operations exist only asEffect.oplabels atperformandhandlesites; there is no place to declare operation signatures, so argument types are checked structurally at each site.- A missing
effectsfield means unknown, not pure.effectschemecarries the complete higher-order graph.
Why effects
Section titled “Why effects”Compared with the usual alternatives:
- Versus exceptions: a handler can
resume, so the computation continues after the operation — exceptions can only unwind. - Versus dependency injection: no interfaces, containers, or mock frameworks; the handler is the injected behavior, scoped lexically.
- Versus monads: effectful code is direct style — no transformer stacks, no lifting, and pure code pays nothing.
Testing falls out for free — handle the effect with canned data:
let getUsers = \() -> perform Db.query "SELECT * FROM users" inhandle getUsers () with Db.query sql resume -> resume [("alice", 30), ("bob", 25)] return val -> valend# => [("alice", 30), ("bob", 25)] (no database anywhere)See the specification for the formal typing rules,
and Concurrency for the built-in Cancel.check effect
used by cooperative cancellation.