Skip to content

Custom Iterators

Zolo's for x in expr is driven by two traits from core::iter:

  • Iterator<T> — a stateful cursor; next(self) -> T? advances it. Returning nil signals exhaustion.
  • IntoIterator<T> — a container that can produce a fresh cursor via iter(self). for x in container calls iter() automatically.

Any type implementing either trait plugs into for with no extra ceremony.

Custom cursor with for, an IntoIterator container, for i, x enumerate, and an adapter chain.

15-custom-iterators.zolo
Playground
// Feature: Custom iterators

// Traits: `Iterator<T>` (cursor) and `IntoIterator<T>` (producer)

// Works on: VM, native, wasm-aot

// Note: adapter chains (.map/.filter/.sum/…) work on VM only.


// `Iter` (the adapter type returned by `iter()`) lives in std.

use std::Iter

// ── 1. Iterator<T> — direct cursor ──────────────────────────────────────────

//

// Implement `Iterator<T>` to make a struct driveable by `for x in`.

// The single required method is `next(self) -> T?`; returning `nil` signals

// the end of the sequence.


struct Countdown { n: int }

impl Iterator<int> for Countdown {
    fn next(self) -> int? {
        if self.n <= 0 { return nil }
        self.n = self.n - 1
        return self.n + 1
    }
}

// ── 2. IntoIterator<T> — container that produces a cursor ───────────────────

//

// Implement `IntoIterator<T>` when the iterable thing is a *container*.

// `iter()` returns a fresh cursor; `for x in container` calls `iter()` first.


struct Bag { items: [int] }

impl IntoIterator<int> for Bag {
    fn iter(self) -> Iter<int> { return Iter.from(self.items) }
}

fn main() {
    // for loop over a direct Iterator

    let c = Countdown { n: 3 }
    for x in c { print(x) }
    // expected: 3

    // expected: 2

    // expected: 1


    // Manual sum via for loop (works on all backends)

    var total = 0
    let c2 = Countdown { n: 3 }
    for x in c2 { total = total + x }
    print(total)
    // expected: 6


    // IntoIterator container

    let b = Bag { items: [10, 20, 30] }
    for x in b { print(x) }
    // expected: 10

    // expected: 20

    // expected: 30


    // for i, x enumerate (0-based index)

    let arr = [100, 200, 300]
    for i, x in arr { print(i) print(x) }
    // expected: 0

    // expected: 100

    // expected: 1

    // expected: 200

    // expected: 2

    // expected: 300


    // Adapter chain on custom iterator — VM only

    // (on native/wasm-aot use the for-loop form above)

    let c3 = Countdown { n: 5 }
    let mapped_sum = c3.map(|x| x * 2).filter(|x| x > 4).sum()
    print(mapped_sum)
    // expected: 24

}

Iterator — direct cursor

struct Countdown { n: int }

impl Iterator<int> for Countdown {
    fn next(self) -> int? {
        if self.n <= 0 { return nil }
        self.n = self.n - 1
        return self.n + 1
    }
}

fn main() {
    let c = Countdown { n: 3 }
    for x in c { print(x) }   // 3, 2, 1
}

IntoIterator — container

struct Bag { items: [int] }

impl IntoIterator<int> for Bag {
    fn iter(self) -> Iter<int> { return Iter.from(self.items) }
}

fn main() {
    let b = Bag { items: [1, 2, 3] }
    for x in b { print(x) }   // 1, 2, 3
}

for x in b desugars to calling b.iter() and driving the returned cursor.

for i, x — enumerate (0-based)

let arr = [10, 20, 30]
for i, x in arr { print(i) print(x) }
// 0  10  1  20  2  30

Works on arrays, IntoIterator containers, and map literals (where i becomes the key and x the value).

Adapter chains — VM only

On the VM, a user iterator can be wrapped into the prelude Iter<T> adapter, giving access to the full map/filter/take/fold/sum/collect/… suite:

let result = Countdown { n: 5 }
    .map(|x| x * 2)
    .filter(|x| x > 4)
    .sum()
// 24

Backend limitation: adapter chains (.map().filter().sum()) and for k, v in mapLiteral destructuring work on the VM only. On native (Cranelift) and wasm-aot these backends lack the Lua Iter.* adapter runtime. Use the for-loop form when you need cross-backend portability.

Generic bounds

A function can accept any Iterator<T> via a generic bound:

fn sum_all<I: Iterator<int>>(it: I) -> int {
    var total = 0
    for x in it { total = total + x }
    return total
}

Iterator is a known trait; the bound is accepted by the compiler.

Runtime limitation — use for inside generic bodies. Calling adapter methods (.fold/.map/…) directly on a generic I: Iterator value fails at runtime on every backend. The generic parameter it is a user struct, not an Iter wrapper, so rawget finds no adapter slot on it. Always use a for x in it { … } loop inside a generic function body.

Note: element-type enforcement across call sites (verifying I is specifically Iterator<int> and not Iterator<str>) is a known follow-up — the compiler currently stores the type arg but does not yet cross-check it at instantiation.

The nil end-sentinel

next() returns T? — a nullable. The end-of-sequence sentinel is nil. This means a sequence whose legitimate elements can be nil cannot be iterated using this protocol. Design such sequences with a wrapper type instead.

Errors

Code Meaning
TE828 for x in expr where expr's type implements neither Iterator nor IntoIterator.

See also

enespt-br