Chapter 24: Generators
Lazy Values on Demand
Sometimes you don't want all the values at once. Maybe there are too many. Maybe computing them is expensive. Maybe the sequence is infinite.
Generators solve this. A generator is a function that produces values one at a time using yield. Each time it yields, it pauses. When you ask for the next value, it resumes right where it left off.
This is lazy evaluation. No work happens until you need the result.
Basic Syntax
A generator looks like a regular function, but returns iter and uses yield instead of return:
fn counting() -> iter {
yield 1
yield 2
yield 3
}
fn main() -> int {
var gen = counting()
print(gen.next()) // 1
print(gen.next()) // 2
print(gen.next()) // 3
return 0
}Each call to .next() runs the generator until the next yield, then pauses. The value after yield is what .next() returns.
When the generator has no more yields, .next() returns none.
Collecting into an Array
If you want all the values at once, use .collect():
fn counting() -> iter {
yield 1
yield 2
yield 3
}
fn main() -> int {
var all = counting().collect()
print(all) // [1, 2, 3]
return 0
}This runs the generator to completion and gathers everything into an array. Useful when you know the sequence is finite and you need all of it.
Generators with Loops
Generators get interesting when combined with loops:
fn range(start: int, end: int) -> iter {
var i = start
while i < end {
yield i
i = i + 1
}
}
fn main() -> int {
var nums = range(0, 5).collect()
print(nums) // [0, 1, 2, 3, 4]
return 0
}The while loop runs, but each iteration pauses at yield. The generator remembers i between calls. This is the key insight-local variables survive across yields.
For-In Iteration
Generators work directly with for-in loops:
fn squares(n: int) -> iter {
var i = 0
while i < n {
yield i * i
i = i + 1
}
}
fn main() -> int {
for x in squares(5) {
print(x)
}
// 0, 1, 4, 9, 16
return 0
}The for-in loop calls .next() repeatedly until the generator is exhausted. No intermediate array is created-values flow directly from the generator to the loop body.
Chaining: map, filter, take
Generators support chaining operations. Each operation returns a new generator, so nothing runs until you consume the result:
fn naturals() -> iter {
var i = 1
while true {
yield i
i = i + 1
}
}
fn main() -> int {
// Get the first 5 even squares
var result = naturals()
.map(fn(x) { return x * x })
.filter(fn(x) { return x % 2 == 0 })
.take(5)
.collect()
print(result) // [4, 16, 36, 64, 100]
return 0
}.map(fn)transforms each value.filter(fn)keeps values where the function returns true.take(n)stops after n values
These are lazy. naturals() is an infinite generator, but .take(5) ensures we only compute what we need. Without .take(), this would run forever.
Practical Example: Fibonacci
The Fibonacci sequence is a classic use case for generators-an infinite sequence where each value depends on the previous two:
fn fibonacci() -> iter {
var a = 0
var b = 1
while true {
yield a
var temp = a + b
a = b
b = temp
}
}
fn main() -> int {
// First 10 Fibonacci numbers
var fibs = fibonacci().take(10).collect()
print(fibs) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Fibonacci numbers under 100
for x in fibonacci() {
if x >= 100 {
break
}
print(x)
}
return 0
}Without generators, you'd need to either precompute the whole sequence or manually track state between calls. The generator handles the bookkeeping for you.
How It Works Under the Hood
Generators compile to coroutines. Here's what happens:
- When you call a generator function, it doesn't run the body. It creates a coroutine object that holds the function's state.
- Each
.next()call resumes the coroutine. It runs until it hits ayield, then suspends. - Local variables are stored in the coroutine object, not on the stack. That's how they survive across yields.
- When the function body ends (or hits a
return), the coroutine is marked as done and.next()returnsnone.
The Wyn compiler transforms your generator into a state machine in the generated C code. Each yield becomes a suspension point with a state label. Resuming jumps back to the right label and continues execution.
This means generators are cheap. No threads, no OS overhead. Just a struct holding local variables and a state index.
Summary
Generators give you lazy sequences with clean syntax:
yieldproduces a value and pauses.next()resumes and gets the next value.collect()gathers all values into an array.map(),.filter(),.take()chain without intermediate allocationsfor-inconsumes generators directly- Under the hood, it's coroutines compiled to C state machines