Skip to content

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.

10-generators.zolo
Playground
// 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.

11-async-coroutines.zolo
Playground
// 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.

enespt-br