Skip to content
Get Started

Algebraic Effects

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.

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
}

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
}

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 value
handle 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 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 effects
fn add(x: Int, y: Int) -> Int ! {} = x + y
// This function performs IO, State, and Int effects
fn 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)
}

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.