Actor Model
The Actor Model
Section titled “The Actor Model”Nulang actors are isolated, concurrent units of computation that communicate exclusively through asynchronous message passing. Each actor has:
- Private state — mutable fields only accessible from within the actor
- Behaviors — message handlers that transform state and send responses
- A mailbox — FIFO-ordered message queue
- An identity — a unique
Actorreference (64-bit id)
Defining Actors
Section titled “Defining Actors”actor Counter { state count: Int = 0
behavior inc() { self.count = self.count + 1 }
behavior inc_by(amount: Int) { self.count = self.count + amount }
behavior get(sender: Actor) { send sender reply(self.count) }}Spawning Actors
Section titled “Spawning Actors”spawn creates a new actor instance and returns its reference:
let counter = spawn Counter {} in { // counter is an Actor reference send counter inc() counter}The expression after in runs in the spawner’s context and can reference the new actor.
Spawn with Link or Monitor
Section titled “Spawn with Link or Monitor”spawn link and spawn monitor create an actor and immediately link or monitor it to the spawner:
// Link: abnormal exits propagate to the spawnerlet worker = spawn link Worker {} in { worker }
// Monitor: spawner receives a DOWN message when worker exitslet worker = spawn monitor Worker {} in { worker }These are parser desugars — spawn link Actor {..} in {..} is equivalent to spawn Actor {..} in { perform Actor.link(actor); .. }, and likewise for monitor. See Supervision Trees for link/monitor semantics and exit trapping.
Sending Messages
Section titled “Sending Messages”send delivers a message to an actor’s mailbox asynchronously:
send counter inc()send counter inc_by(5)send counter get(self) // self refers to the current actorMessages are always delivered and never dropped. The sender does not block.
Receiving Messages
Section titled “Receiving Messages”Inside a behavior, receive blocks until a matching message arrives:
behavior wait_for_reply() { receive { | reply(value: Int) => { perform IO.print("Got: " + Int.to_string(value)) } }}The receive expression supports selective matching — only matching messages are consumed; non-matching messages are requeued:
receive { | reply(value: Int) => { // Handle integer reply perform IO.print("Int: " + Int.to_string(value)) } | reply(msg: String) => { // Handle string reply perform IO.print("String: " + msg) }}Timed Receive
Section titled “Timed Receive”Wait for a message with a timeout:
receive { | reply(value: Int) => perform IO.print("Got it")} after 5000 => { perform IO.print("Timed out after 5 seconds")}How timed receive works
Section titled “How timed receive works”When no matching message is in the mailbox and the timeout is positive, the actor suspends. The runtime:
- Suspends the actor, capturing its execution state (frames, PC, registers).
- Arms a one-shot timer for the timeout duration.
- Resumes the actor when either a matching message arrives or the timer fires.
If the timer fires first, the after body runs. If a matching message arrives first, the matching arm runs and the timer is cancelled. The actor stays suspended and re-suspends if a non-matching message arrives (it is requeued, not consumed).
Suspension only happens inside an actor context with the runtime enabled. Outside an actor (e.g. in a standalone --eval script), receive with after resolves synchronously — it returns immediately when no match is found rather than suspending.
Actor Lifecycle
Section titled “Actor Lifecycle”- Spawned — Actor is created with initial state
- Running — Processing messages from the mailbox
- Waiting — Blocked in a
receivewith no matching messages - Exited — Actor terminated (normal, error, or killed)
Actors can self-exit:
perform Actor.exit(0) // Normal exitperform Actor.exit(1) // Error exitperform Actor.exit(2) // Kill (cannot be trapped)Scheduling
Section titled “Scheduling”Actors are scheduled cooperatively with reduction-bounded fairness. Each actor gets a budget of 1000 message reductions per turn. When exhausted, it yields and requeues at the back of its priority queue.
Priorities: High → Normal (default) → Low. All High-priority actors run before any Normal; Normal before Low.
- Distribution & Clustering — run actors across multiple nodes
- Supervision Trees — build fault-tolerant systems with OTP supervisors