Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 operations
  • handler — defines an implementation of an effect
  • with — 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:

KindMeaningContinuation
fnTail-resumptive: always resumes exactly onceNo continuation captured (cheap)
ctlControl: may resume zero or many timesCaptures continuation
finalFinal: never resumesNo 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 :::

KindMeaning
::VValue type (default)
::SScope token
::EEffect row
::HHeap/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)