Algebraic Effects
Overview
Section titled “Overview”Algebraic effects let you separate what a program does from how it does it. An effect declares operations; handlers provide implementations. Effects compose freely without monad transformers.
Defining Effects
Section titled “Defining Effects”Effects are declared with effect followed by a name and a block of operation signatures. Each operation is name: (arg_types) -> return_type:
effect State { get: () -> Int put: (Int) -> Unit}
effect Logger { log: (String) -> Unit}Performing Effects
Section titled “Performing Effects”perform invokes an operation. The compiler statically tracks which effects each function may perform via row-polymorphic effect types:
fn counter() -> Int ! {State} { let current = perform State.get() perform State.put(current + 1) current}Handling Effects
Section titled “Handling Effects”handle intercepts effect operations and provides implementations. The handler arm’s body expression is the value resumed into the continuation:
// Handle State with a fixed initial valuehandle perform State.get() { | State.get() => 42}When the handler needs to perform side effects before providing the resume value, use the resume keyword in the arm pattern. The arm body becomes the resumed value:
handle perform Logger.log("hello") { | Logger.log(msg) resume => { perform IO.print("[LOG] " + msg) }}The resume keyword marks that execution should continue at the perform site with the arm body’s value. Without resume, the handler arm’s value is the handle expression’s result (the handler does not resume the continuation).
Effect Rows
Section titled “Effect Rows”Effect types use row polymorphism, like records. The effect row appears in a function’s type signature after the return type, introduced by ! or throws:
// Pure function — no effectsfn add(x: Int, y: Int) -> Int ! {} = x + y
// This function performs IO, State, and Int effectsfn program() -> Unit ! {IO, State, Int} { perform IO.print("Running...") let v = perform State.get() perform IO.print(perform Int.to_string(v))}throws is an alias for !:
fn log(msg: String) -> Unit throws {IO} { perform IO.print(msg)}Built-in Effects
Section titled “Built-in Effects”Nulang ships with built-in effects wired into the VM and runtime. The canonical
list lives in the standard library registry (src/stdlib.rs); the families
are:
| Effect | Operations | Description |
|---|---|---|
IO |
print, println, read, log, log_error |
Console I/O and logging |
Int |
to_string, to_float, to_hex, to_binary |
Integer conversions |
Float |
to_int, to_string, sin, cos, sqrt, pow, … |
Float conversions and math |
String |
length, concat, substring, charAt, to_int, to_float |
String operations |
Array |
length, new, push, set, slice |
Array operations |
Map |
new, insert, get, contains, remove, size |
Hash-map operations |
StrBuilder |
new, push, to_string, len, reset |
Efficient string building |
FS |
read, write, append, exists |
File-system access |
Env |
get |
Environment variables |
Process |
run |
Shell command execution |
Debug |
inspect |
Labeled value inspection |
Test |
assert, assert_eq, assert_true |
Test assertions |
Random |
int |
Random numbers |
Time |
now |
Wall-clock time |
Timer |
sleep |
Durable workflow timers |
Signal |
wait |
Workflow signal suspension |
Inference |
ask |
AI language model queries (canonical name) |
Actor |
link, unlink, monitor, demonitor, trap_exit, exit, register, unregister, whereis, set_priority |
Actor lifecycle management |
Otp |
create_supervisor, supervise_child, set_template, start_child, terminate_child, child_count |
OTP supervision trees |
Crdt |
increment, decrement, add, remove, set, read |
CRDT-backed actor state |
Http |
get, post, serve |
HTTP client and server |
Web |
html, text, route, param, header, cookie, … |
Web framework primitives |
Python |
import, call, get_attr |
Python interop (PyO3) |
System |
arg |
Command-line arguments |
Realtime |
broadcast |
Realtime channel broadcast |
perform LLM.ask(prompt) remains accepted as a legacy alias for
perform Inference.ask(prompt) — both dispatch identically at runtime.
Provider.ask(service, prompt) is not a stdlib-registry effect; it is
dispatched directly through the runtime’s provider callbacks (the longevity
path for swappable service providers).
See the Standard Library for full documentation of each built-in effect, with per-effect pages covering every operation and its signature.