Generators and Coroutines
A generator is declared with fn* and uses yield to produce a value
and suspend execution. Calling the returned handle advances to the next
yield; when the body ends, it returns nil. Because execution is lazy,
infinite sequences are possible:
Finite counter, infinite Fibonacci, parameterized range_step, and a string generator.
// Feature: Generators — functions that produce values on demand
// Syntax: `fn* name(...)` + `yield <value>` in the body.
// When to use: infinite sequences, lazy evaluation, streams,
// pipelines where we don't want to materialize everything in memory.
//
// Each call to the generator returns the next `yield`. When the
// function ends, it returns `nil`.
// Finite counter.
fn* counter(limit: int) {
var i = 0
while i < limit {
yield i
i += 1
}
}
let gen = counter(5)
print(gen()) // 0
print(gen()) // 1
print(gen()) // 2
print(gen()) // 3
print(gen()) // 4
print(gen()) // nil (exhausted)
// INFINITE sequence — possible because it's lazy.
fn* fibonacci() {
var a = 0
var b = 1
loop {
yield a
let temp = a + b
a = b
b = temp
}
}
let fib = fibonacci()
for i in 0..10 {
print("fib({i}) = {fib()}")
}
// Parameterized generator.
fn* range_step(start: int, stop: int, step: int) {
var i = start
while i < stop {
yield i
i += step
}
}
print("Evens 0-10:")
let evens = range_step(0, 10, 2)
var v = evens()
while v != nil {
print(" {v}")
v = evens()
}
// String generator — any type works.
fn* statuses() {
yield "loading"
yield "processing"
yield "done"
}
let st = statuses()
var s = st()
while s != nil {
print("status: {s}")
s = st()
}
async fn is syntactic sugar for a coroutine based on the same mechanism.
The handle is advanced the same way as a generator, which makes
cooperation between tasks explicit — you control when each "task"
yields the processor:
produce_numbers, producer/consumer, and interleaving of two cooperative tasks.
// Feature: async fns / coroutines (built on generators)
// Syntax: `async fn name()` produces a coroutine; uses `yield`
// to suspend and hand control back.
// When to use: cooperative tasks, step-wise simulation of I/O,
// lightweight state machines.
//
// In Zolo, `async fn` is sugar for a coroutine — you advance it
// by calling the returned handle, just like a generator.
async fn produce_numbers() {
yield 10
yield 20
yield 30
}
let producer = produce_numbers()
print(producer()) // 10
print(producer()) // 20
print(producer()) // 30
// `fn*` also works for stream-style asynchronous sources.
fn* async_data() {
yield "loading..."
yield "processing..."
yield "done!"
}
let status = async_data()
var s = status()
while s != nil {
print("Status: {s}")
s = status()
}
// Producer/consumer: the consumer "pulls" items.
fn* producer_items(items: [str]) {
for item in items {
yield item
}
}
let gen = producer_items(["apple", "banana", "cherry"])
var item = gen()
while item != nil {
print("Consumed: {item}")
item = gen()
}
// Multiple "tasks" cooperating: we advance them in turn.
fn* task_a() {
yield "a1"
yield "a2"
yield "a3"
}
fn* task_b() {
yield "b1"
yield "b2"
}
let a = task_a()
let b = task_b()
print(a()) // a1
print(b()) // b1
print(a()) // a2
print(b()) // b2
print(a()) // a3
print(b()) // nil
Challenge
Write a generator fn* naturals() that produces 1, 2, 3, … infinitely.
Use while v != nil to print only the first 10.
See also