Skip to content
Try Nulang

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 Trusted/standalone shell command execution
Debug inspect Labeled value inspection

Process.run is intentionally not dispatched by actor-backed production runtimes. The standalone VM implementation uses /bin/sh -c, which inherits host OS authority and is therefore unsuitable for untrusted actor execution until a separate isolated process sandbox with explicit resource and filesystem/network policy exists. | 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.