Lazy Iterators in Zolo: Infinite Sequences Done Right
How Zolo's lazy iterator system lets you work with infinite sequences without running out of memory — and why it changes how you think about data.
Lazy Iterators in Zolo: Infinite Sequences Done Right
One of Zolo's most powerful features is its lazy iterator system. Unlike eager collections that compute everything upfront, lazy iterators only compute values when asked. This means you can work with infinite sequences without running out of memory.
What Is Lazy Evaluation? #
In an eager language:
[1, 2, 3, 4, 5, ...∞] → mapped → filtered → first 5
The entire sequence is computed before you can take the first 5 elements. With infinite sequences, this crashes.
In a lazy system:
take 5 ← filter ← map ← [1, 2, 3, ...]
Values are pulled from the source only when the consumer needs them. No intermediate arrays are allocated.
The 0.. Infinite Range
Zolo's 0.. syntax creates an infinite integer range:
let naturals = 0.. // lazy, no memory allocated
By itself, this does nothing. You need to consume it:
let first_ten = 0..
|> Iter.take(10)
|> Iter.collect()
print(first_ten) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Core Iterator Operations #
Iter.map — Transform Each Element
let squares = 0..
|> Iter.map(|x| x * x)
|> Iter.take(6)
|> Iter.collect()
print(squares) // [0, 1, 4, 9, 16, 25]
Iter.filter — Keep Matching Elements
let evens = 0..
|> Iter.filter(|x| x % 2 == 0)
|> Iter.take(5)
|> Iter.collect()
print(evens) // [0, 2, 4, 6, 8]
Iter.take_while — Stop When Condition Fails
let small_squares = 0..
|> Iter.map(|x| x * x)
|> Iter.take_while(|x| x < 100)
|> Iter.collect()
print(small_squares) // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Iter.fold — Aggregate Without Collecting
// Sum of first 100 natural numbers
let sum = 0..
|> Iter.take(100)
|> Iter.fold(0, |acc, x| acc + x)
print(sum) // 4950
Iter.zip — Pair Two Iterators
let a = 0..
let b = 0.. |> Iter.map(|x| x * x)
let pairs = Iter.zip(a, b)
|> Iter.take(5)
|> Iter.collect()
// [(0,0), (1,1), (2,4), (3,9), (4,16)]
print(pairs)
Custom Iterators with Generators #
The fn* syntax creates generator functions — they use yield to emit values one at a time:
fn* fibonacci() {
let mut a = 0
let mut b = 1
loop {
yield a
let next = a + b
a = b
b = next
}
}
let fibs = fibonacci()
|> Iter.take(12)
|> Iter.collect()
print(fibs) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Iter.from_fn — Iterator from a Closure
fn counter(start: int, step: int) {
let mut n = start
return Iter.from_fn(|| {
let val = n
n = n + step
return val
})
}
let by_threes = counter(0, 3)
|> Iter.take(5)
|> Iter.collect()
print(by_threes) // [0, 3, 6, 9, 12]
Chaining Complex Pipelines #
The real power comes from chaining multiple operations. The entire chain is lazy — memory stays flat:
// Find the 10th prime number
fn is_prime(n: int) -> bool {
if n < 2 { return false }
let mut i = 2
while i * i <= n {
if n % i == 0 { return false }
i = i + 1
}
return true
}
let tenth_prime = 2..
|> Iter.filter(is_prime)
|> Iter.nth(9) // 0-indexed
print(tenth_prime) // 29
Performance: Why Laziness Wins #
// Eager: allocates [0..999999], then [0..499999] filtered
// Lazy: allocates NOTHING until collect()
let result = 0..1_000_000
|> Iter.filter(|x| x % 2 == 0)
|> Iter.map(|x| x * x)
|> Iter.take(5)
|> Iter.collect()
print(result) // [0, 4, 16, 36, 64]
// Only 5 elements ever computed
Conclusion #
Lazy iterators change how you think about data transformation. Instead of "allocate, compute, discard", you think in terms of pipelines that flow data on demand.
Combined with Zolo's pipe operator |>, iterators become a natural, composable way to express complex data processing with minimal overhead.
Explore all iterator functions in the stdlib docs.