Effects & Handlers
Ore uses algebraic effects for side effects, resource management, and control flow. The system is modeled after Koka and has three core constructs:
effect— declares an effect and its operationshandler— defines an implementation of an effectwith— installs a handler for a scope
Declaring Effects
An effect declares a set of operations that code can perform:
effect Console
fn print(msg: String)
fn read_line() -> String
Operation Kinds
There are three kinds of effect operations:
| Kind | Meaning | Continuation |
|---|---|---|
fn | Tail-resumptive: always resumes exactly once | No continuation captured (cheap) |
ctl | Control: may resume zero or many times | Captures continuation |
final | Final: never resumes | No continuation (like exceptions) |
effect Async<a>
fn spawn(task: fn() -> a) -> a // always resumes
ctl yield() -> a // may suspend/resume
ctl abort(reason: String) -> Never // may not resume
effect Error
final throw(msg: String) -> Never // never resumes
fn is the cheapest — use it when the handler always returns a value and continues. ctl captures a continuation, which the handler can call to resume. final is an optimization hint: the compiler knows no continuation is needed.
Scoped Effects
A scoped effect creates a fresh scope token each time it is handled, preventing handlers from being used outside their scope:
scoped effect heap<s::S>
ctl new_ref(init: a) -> ref<s, a>
The s::S annotation means s is a scope kind parameter — it’s not a regular type, but a token that ties references to their heap.
Named Effects
A named effect is an effect instance that can be passed as a first-class value. Named effects declare their containing scoped effect with in:
named effect ref<s::S, a::V> in heap<s>
fn get() -> a
fn set(value: a)
This says ref is an instance of an effect that lives inside a heap<s> scope. You can have multiple ref instances within the same heap.
Kind Annotations
Type parameters can have kind annotations using :::
| Kind | Meaning |
|---|---|
::V | Value type (default) |
::S | Scope token |
::E | Effect row |
::H | Heap/handler |
scoped effect heap<s::S> // s is a scope token
named effect ref<s::S, a::V> // s is scope, a is a value type
Marker Effects
Effects with no operations:
effect Pure
Generics, Bounds, and Where Clauses
effect Transform<a: Clone>
fn transform(value: a) -> a
effect Convert<a, b> where a: Clone, b: From<a>
fn convert(value: a) -> b
Visibility and Attributes
pvt effect InternalEffect
fn internal_op() -> i64
#[some_attr]
effect Documented
fn describe() -> String
Everything Combined
#[some_attr]
pvt effect FullEffect<a, b: Default> where a: Clone
fn required(value: a) -> b
ctl fail(msg: String) -> Never
final panic(msg: String) -> Never
Effect Rows
Effects appear in function signatures as effect rows — a list of effects the function may perform. Effect rows appear before the return type:
fn read_file(path: String) -> <io> String
// performs the io effect
fn complex(x: i64) -> <io, state> String
// performs io and state effects
Open Rows (Polymorphic Effects)
A row variable e makes effects polymorphic — the function can perform whatever additional effects the caller has:
fn map(xs: List<a>, f: fn(a) -> e b) -> e List<b>
// ...
Here e is an effect variable — map performs whatever effects f performs. A bare e before the return type is shorthand for an open effect row.
Explicit row syntax with a tail:
fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a
// action performs heap<s> + div + whatever e is
The |e separates named effects from the row tail variable.
Forall
The forall quantifier introduces scoped type/effect variables, commonly used with scoped effects:
fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a
The forall<s> ensures s is fresh for each call — references created inside can’t escape.
Handlers
A handler expression defines how to handle an effect’s operations.
Basic Handler
fn state_handler(init: i64)
var state = init
handler State
fn get() { state }
fn set(s) { state = s }
Operation parameters are just names — types come from the effect definition.
Named Handler
For named effects, use named handler:
fn with_ref(init: a, action: fn(ref<s, a>) -> e b) -> e b
var state = init
with r <- named handler
fn get() { state }
fn set(x) { state = x }
action(r)
The r binding captures the handler instance as a first-class value.
Return Clause
Process the final value of the handled computation:
handler State
fn get() { state }
fn set(s) { state = s }
return(val) { val }
Initially and Finally
Resource management without special destructor hooks:
fn file_handler(path: String)
let fd = Os::open(path)
handler File
initially { print("opening") }
finally { Os::close(fd) }
fn read(buf, len) { Os::read(fd, buf, len) }
fn write(data) { Os::write(fd, data) }
initially runs when the handler is installed. finally runs when the handled scope exits (whether normally or via a final operation).
Resume
In ctl handlers, resume(value) continues the captured continuation with a value:
fn example() -> i64
with handler Ask
ctl ask() { resume(42) }
Ask::ask() + 1 // ask() resumes with 42, so this returns 43
Not calling resume short-circuits — the handler’s value becomes the result of the entire with block:
fn safe_div(x: i64, y: i64) -> i64
with handler Fail
ctl fail(msg) { -1 } // no resume → with block returns -1
if y == 0 { Fail::fail("division by zero") }
x / y
resume can be called multiple times for effects like non-determinism:
fn example() -> List<i64>
with handler Choose
ctl choose()
let a = resume(true)
let b = resume(false)
a ++ b
let x = if Choose::choose() { 1 } else { 2 }
[x]
fn operations auto-resume (no explicit resume needed). final operations never resume.
With Expressions
with installs a handler for a block of code.
Rest of Scope
with installs a handler that covers the rest of the enclosing scope. It does not return a value — it establishes a scope.
fn example() -> i64
with state(0)
State::set(10)
State::get()
Handler Binding
Use <- to bind a named handler instance:
fn main()
with heap
let r1 = new_ref(41)
let r2 = dynamic_ref(true)
let v2 = if r2.get() { 1 } else { 0 }
println(r1.get() + v2)
Named handlers are bound with:
with r <- named handler
fn get() { state }
fn set(x) { state = x }
Stacking Handlers
Multiple with statements stack flat — each covers the rest of the scope:
fn example() -> i64
with diagnostic_stderr()
with state_handler(0)
State::set(42)
State::get()
Single-Operation Shorthand
For handlers with a single operation, skip the handler keyword — specify the operation kind and name directly:
fn example() -> i64
with ctl fail(msg)
-1
safe_div(10, 0)
fn example2() -> i64
with ctl ask()
resume(42)
Ask::ask() + 1
fn example3() -> i64
with fn get()
42
State::get()
This is equivalent to with handler Effect: op(params) { body } but more concise.
Override
override with re-handles an effect that is already in scope, replacing its handler.
The key difference from regular with: a regular handler is not in scope for its own body. If you call the same effect operation inside the handler, it escapes to an outer handler. override with makes the handler visible to itself, enabling recursion.
Delegating to the Outer Handler
fn wrap_ref(action: fn() -> e a) -> e a
override with fn get() -> a
get() // calls the SAME handler (recursive)
action()
Recursive Handlers
This is the primary use case. If your handler’s logic needs to call its own operation (e.g., open resolving a symlink by calling open again), override is required:
// Regular 'with': the inner open() escapes to a DIFFERENT handler above
with fn open(path)
if is_symlink(path) { open(target) } // looks for outer handler!
// 'override with': the inner open() calls back into THIS handler
override with fn open(path)
if is_symlink(path) { open(target) } // recursive — same handler
Without override, the default behavior prevents accidental infinite loops and allows layering (e.g., a file-logger that calls println, handled by a different console-logger above).
Mask
mask<effect> { body } temporarily hides an effect from the inner scope, so operations in the body skip the nearest handler and go to an outer one:
fn skip_inner(action: fn() -> e a) -> e a
let x = mask<ref<s, a>> { get() } // get() skips the inner ref handler
action()
This is the inverse of override. Where override makes a handler visible to itself, mask makes a handler invisible to the inner scope. This is useful when you need to reach past a local handler to access an outer one:
fn example()
with handler Logger
fn log(msg) { print("[outer] " ++ msg) }
with handler Logger
fn log(msg) { print("[inner] " ++ msg) }
log("hello") // goes to inner handler
mask<Logger> { log("hello") } // skips inner, goes to outer handler
Full Example: Heap
This example shows scoped effects, named effects, handlers, override, and mask working together:
scoped effect heap<s::S>
ctl new_ref(init: a) -> ref<s, a>
named effect ref<s::S, a::V> in heap<s>
fn get() -> a
fn set(value: a)
fn with_ref(init: a, action: fn(ref<s, a>) -> e b) -> e b
var state = init
with r <- named handler
fn get() { state }
fn set(x) { state = x }
action(r)
fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a
with ctl new_ref(init: a)
with_ref(init, |v| resume(v))
action()
fn main()
with heap
let r1 = new_ref(41)
let r2 = dynamic_ref(true)
let v2 = if r2.get() { 1 } else { 0 }
println(r1.get() + v2)