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 has three 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 State<S>:
  fn get() -> S
  fn set(s: S)

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<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.