Generators

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

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:

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:

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:

// 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 #

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]
enespt-br