Skip to content
Get Started

Tutorial

Welcome to Nulang! This tutorial walks you through the language from first principles. By the end, you’ll be writing actors, handling effects, and building distributed systems.

Install Nulang by building from source:

Terminal window
git clone https://github.com/nulang-org/nulang.git
cd nulang
cargo build --release

Verify your installation:

Terminal window
cargo run -- --version
cargo run -- --repl

For detailed platform-specific instructions, see the Installation page.

Nulang uses algebraic effects for I/O. Print to the console with perform IO.print:

perform IO.print("Hello, Nulang!")

Run it:

Terminal window
cargo run -- --eval 'perform IO.print("Hello, Nulang!")'

Or start the REPL and type interactively:

Terminal window
cargo run -- --repl
nulang> perform IO.print("Hello, Nulang!")

Bind values with let and define functions with fn:

let x = 42
let greeting = "Hello"
fn add(a: Int, b: Int) -> Int {
a + b
}
perform IO.print(perform Int.to_string(add(x, 8)))

Closures capture their environment:

let base = 10
let add_base = fn(x) { x + base }
perform IO.print(perform Int.to_string(add_base(5))) // 15

Nulang has Hindley-Milner type inference — types are usually optional. Declare custom variant types and match on them:

type Option[T] = Some(T) | None
fn unwrap_or(opt: Option[Int], default: Int) -> Int {
match opt with {
| Some(x) => x
| None => default
}
}
let present = Some(42)
let absent = None
perform IO.print(perform Int.to_string(unwrap_or(present, 0))) // 42
perform IO.print(perform Int.to_string(unwrap_or(absent, 99))) // 99

Pattern matching supports guards, nested patterns, records, and wildcards:

type Color = Red | Green | Blue
fn describe(c: Color) -> String {
match c with {
| Red => "warm"
| Green => "natural"
| Blue => "cool"
}
}
fn classify(n: Int) -> String {
match n with {
| x if x < 0 => "negative"
| 0 => "zero"
| _ => "positive"
}
}

Actors are the core concurrency primitive. Each actor has private state and a set of named behaviors:

actor Counter {
state count: Int = 0
behavior inc() {
self.count = self.count + 1
}
behavior get() { self.count }
}

Spawn an actor, call behaviors with ask, or fire-and-forget with send:

actor Counter {
state count: Int = 0
behavior inc() { self.count = self.count + 1 }
behavior get() { self.count }
}
let counter = spawn Counter {}
let result = ask counter get() // 0
ask counter inc()
ask counter inc()
let result2 = ask counter get()
perform IO.print(perform Int.to_string(result2)) // 2

Actors can link together for fault tolerance, register names in the global registry, and monitor each other’s lifecycle:

actor Worker {
behavior start(name: String) {
perform Actor.register(name)
perform IO.print(name + " registered")
}
behavior work() {
perform Actor.exit(0) // normal shutdown
}
}

See Distributed Actors for distribution, clustering, and supervision trees.

Effects are Nulang’s mechanism for side-effect management. The compiler tracks which effects each function may perform, and handlers intercept those effects at runtime:

effect Logger {
log: (String) -> Unit
}
fn greet(name: String) {
perform Logger.log("greeting " + name)
perform IO.print("Hello, " + name)
}
handle greet("World") {
| Logger.log(msg) resume => {
perform IO.print("[LOG] " + msg)
}
}

Built-in effects include IO, Timer, Signal, Inference (LLM), Actor, and Otp (supervisors). See the Standard Library for the complete reference.

You’ve covered the foundations. Here’s where to go next:

  • Language Syntax — deep dive into expressions, declarations, and modules
  • Type System — reference capabilities, row polymorphism, and effect rows
  • Distributed Actors — clustering, location transparency, and CRDTs
  • AI Agents — LLM integration, pipelines, debates, and supervisor teams
  • Durable Workflows — signals, timers, queries, and saga transactions

Browse the full API Reference or explore the examples on GitHub.