Skip to content

Guide 16 of 53

Generators

Generator functions with fn* and yield for lazy sequences.

On this page

Generators are functions that can pause their execution and produce a sequence of values lazily. They are defined with fn* and use the yield keyword to emit values one at a time.

Basic Generator

fn* count_up(start: int, end: int) {
    let mut i = start
    while i <= end {
        yield i
        i = i + 1
    }
}

for n in count_up(1, 5) {
    print(n)
}
// 1
// 2
// 3
// 4
// 5

Delegating a sequence

Use yield ... to forward the values of another iterator:

fn* middle() -> int {
    yield 2
    yield 3
}

fn* numbers() -> int {
    yield 1
    yield ... middle()
    yield 4
}

for value in numbers() { print(value) }
// 1, 2, 3, 4

Delegation acquires the child once when execution reaches that statement. It requests one child value for each pull of the parent, then continues the parent when the child is exhausted. It does not build an intermediate array. A delegated value must have a checked iterator contract; a scalar or an erased value reports TE198.

An owned nominal cursor needs the canonical Iterator and Cleanup witnesses and a checked construction. A borrowed cursor parameter does not prove that ownership merely by declaring an iterator type. The nominal bridge currently accepts non-generic next(self) and close(self) implementations.

The parent owns the delegated cursor. Exhaustion, an early close, a loop exit or an ordinary failure closes the child and runs cleanup before closing the parent. Closing an already closed cursor is harmless. A delegated iterator can yield nil as an item when its element type permits it; ordinary yield nil retains the existing end-of-sequence rule.

The spread must follow yield directly. yield [1, ... values] still yields one array; yield (... values) is not delegation.

JavaScript and standalone wasm-aot report TE198 for delegated yields. Ordinary simple generators remain supported by wasm-aot.

Infinite Generators

Generators can be infinite — they produce values on demand:

fn* naturals() {
    let mut n = 0
    loop {
        yield n
        n = n + 1
    }
}

fn* fibonacci() {
    let mut a = 0
    let mut b = 1
    loop {
        yield a
        let next = a + b
        a = b
        b = next
    }
}

Use Iter.take to consume a finite number of values from an infinite generator:

use std::Iter

let first10 = fibonacci()
    |> Iter.take(10)
    |> Iter.collect()

print(first10)
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator with Parameters

fn* range(start: int, end: int, step: int) {
    let mut i = start
    while i < end {
        yield i
        i = i + step
    }
}

// Every 3rd number from 0 to 20
for n in range(0, 20, 3) {
    print(n)
}
// 0, 3, 6, 9, 12, 15, 18

Using Generators with Iter

Generators implement the Iter protocol, so all iterator combinators work:

use std::Iter

fn* squares() {
    let mut n = 0
    loop {
        yield n * n
        n = n + 1
    }
}

// First 5 even squares
let result = squares()
    |> Iter.filter(|x| x % 2 == 0)
    |> Iter.take(5)
    |> Iter.collect()

print(result)  // [0, 4, 16, 36, 64]

Early Return from Generator

A generator can return early, stopping the sequence:

fn* bounded_fib(max: int) {
    let mut a = 0
    let mut b = 1
    while a <= max {
        yield a
        let next = a + b
        a = b
        b = next
    }
    // generator ends here
}

for n in bounded_fib(100) {
    print(n)
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

Yield with Expressions

yield can return any expression:

fn* enumerate(items: [str]) {
    let mut i = 0
    for item in items {
        yield (i, item)
        i = i + 1
    }
}

for (i, name) in enumerate(["Alice", "Bob", "Carol"]) {
    print("{i}: {name}")
}
// 0: Alice
// 1: Bob
// 2: Carol

Generator vs Iter.from_fn

For simple cases you can also use Iter.from_fn with a closure that returns the next value:

use std::Iter

// Generator style
fn* counter() {
    let mut n = 0
    loop { yield n; n = n + 1 }
}

// Iter.from_fn style
fn counter2() {
    let mut n = 0
    return Iter.from_fn(|| {
        let val = n
        n = n + 1
        return val
    })
}

Both produce identical lazy sequences. Generators are usually cleaner for complex stateful logic.

Collecting and Chaining

use std::Array
use std::Iter

fn* primes() {
    let mut seen = []
    let mut candidate = 2
    loop {
        let is_prime = seen
            |> Array.all(|p| candidate % p != 0)
        if is_prime {
            yield candidate
            seen = [...seen, candidate]
        }
        candidate = candidate + 1
    }
}

let first_primes = primes()
    |> Iter.take(10)
    |> Iter.collect()

print(first_primes)
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

DOCS / FEEDBACK

Did this page leave a question?

Tell us where the explanation lost you. Documentation is part of the language experience.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

en