Effects & Handlers
Ore uses algebraic effects for side effects, resource management, and control flow. The system has three 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 State<S>:
fn get() -> S
fn set(s: S)
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<T>:
fn spawn(task: fn() -> T) -> T // always resumes
ctl yield() -> T // 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.
Marker Effects
Effects with no operations:
effect Pure
Super Effects
An effect can extend others with with:
effect FileIO with IO:
fn read_file(path: String) -> String
fn write_file(path: String, content: String)
effect NetworkIO with IO + Exception:
fn fetch(url: String) -> String
Generics, Bounds, and Where Clauses
effect Transform<T: Clone>:
fn transform(value: T) -> T
effect Convert<T, U> where T: Clone, U: From<T>:
fn convert(value: T) -> U
Visibility and Attributes
pvt effect InternalEffect:
fn internal_op() -> i64
#[some_attr]
effect Documented:
fn describe() -> String
Everything Combined
#[some_attr]
pvt effect FullEffect<T, U: Default> with IO + Exception where T: Clone:
fn required(value: T) -> U
ctl fail(msg: String) -> Never
final panic(msg: String) -> Never
Declaring Effects on Functions
Functions declare which effects they perform with !(...):
fn read_file(path: String) -> String !(IO):
// ...
fn complex(x: i64) -> String !(IO + State):
// ...
Handlers
A handler expression defines how to handle an effect’s operations. Operation parameters are just names (types come from the effect definition):
fn simple_handler():
handler State:
get(): state
set(s): state = s
Return Clause
Process the final value of the handled computation:
fn state_handler(init: i64):
state = init
handler State:
get(): state
set(s): state = s
return(val): val
Initially and Finally
Resource management without a Drop trait:
fn file_handler(path: String):
let fd = Os::open(path)
handler File:
initially: print("opening")
finally: Os::close(fd)
read(buf, len): Os::read(fd, buf, len)
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).
All Clauses
fn full_handler():
let count = 0
handler Logger:
initially: print("logger started")
finally: print("logger stopped")
log(msg):
count = count + 1
print(msg)
get_count(): count
return(val): (val, count)
Resume
In ctl handlers, resume(value) continues the captured continuation with a value:
fn example() -> i64:
with handler Ask:
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:
fail(msg):
-1 // no resume → with block returns -1
when 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:
choose():
let a = resume(true)
let b = resume(false)
a ++ b
let x = when 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.
Scoped Body
fn example() -> i64:
let (state, x) = with state_handler(0):
State::set(42)
State::get()
x + 1
Rest of Scope
Without a : body, the handler covers the rest of the enclosing scope:
fn example() -> i64:
with state(0)
State::set(10)
State::get()
Nested Handlers
fn example() -> i64:
with diagnostic_stderr():
with state_handler(0):
State::set(42)
State::get()
Inline Handler
Define and install a handler in one expression:
fn example() -> i64:
let x = with handler State:
get(): 42
set(s): state = s
x
Single-Operation Shorthand
For handlers with a single operation, skip the handler keyword — the effect is inferred from the operation name:
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 for single-operation cases.