Skip to content

Fundamentals

On this page

An effect is an interface of impure operations. The effect block declares only signatures — no bodies. A function that uses the effect annotates with EffectName in its signature and invokes each operation with perform. The caller decides the real meaning of each operation inside a handle … with { … } block.

The simplest example has a single effect with a single operation. The handler provides the only required clause, and the value of the clause expression is what perform returns:

effect IO with one operation; handler returns a default string.

01-basic.zolo
Playground
// Feature: algebraic effects — declare, perform, handle

// Syntax: `effect Name { fn op(...) -> T }` declares; `perform Name::op(...)`

// raises; `handle expr with { Name::op(args) => ... }` interprets.

// When to use: dependency-inject impure ops (IO, logging, time,

// randomness) without dragging trait objects through every call.


effect IO {
  fn read(path: str) -> str
}

// `with IO` says: this function performs at least one IO effect.

fn read_config() with IO -> str {
  return perform IO::read("config.toml")
}

// The handler block decides what `IO::read` actually means.

let result = handle read_config() with {
  IO::read(path) => "default config from {path}",
}

print(result)
// expected: default config from config.toml

An effect can group several related operations — for example, a Logger with info and warn. The handler must cover all operations the body may invoke:

Logger with info/warn; the handler dispatches each one to print.

02-multiple-ops.zolo
Playground
// Feature: effect with multiple operations

// Syntax: an `effect` block can declare several `fn` operations;

// the handler must provide a clause for every operation that

// the effectful code may `perform`.

// When to use: a coherent capability — e.g. a Logger with `info`/`warn`/`error`

// — that you want to swap atomically (test logger, file logger, …).


effect Logger {
  fn info(msg: str)
  fn warn(msg: str)
}

fn run() with Logger {
  perform Logger::info("starting")
  perform Logger::warn("careful")
  perform Logger::info("done")
}

handle run() with {
  Logger::info(msg) => print("[INFO] {msg}"),
  Logger::warn(msg) => print("[WARN] {msg}"),
}
// expected:

//   [INFO] starting

//   [WARN] careful

//   [INFO] done

Handler clauses receive the operation's arguments as named parameters. The value produced by the clause expression is returned to perform as the result — which allows deterministic mocks and computed stubs:

Math effect with add and double; handler computes the real result from the arguments.

03-handler-args.zolo
Playground
// Feature: handler clauses can use the operation arguments

// Syntax: each clause `Op(arg1, arg2) => <expr>` binds the args

// and the expression's value is the result of `perform`.

// When to use: any effect where the result depends on the input —

// pure mocks, deterministic stubs, computed responses.


effect Math {
  fn add(a: int, b: int) -> int
  fn double(n: int) -> int
}

fn compute() with Math -> int {
  let x = perform Math::add(2, 3)
  let y = perform Math::double(x)
  return y
}

let result = handle compute() with {
  Math::add(a, b) => a + b,
  Math::double(n) => n * 2,
}
print(result)
// expected: 10

Challenge

In the Logger example, add a third operation error(msg: str) to the effect and a matching clause in the handler. What happens if you omit the clause?

Search Zolo

9 results

enespt-br