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.
On this page
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 5The 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 allocatedBy 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) // 4950Iter.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) // 29Performance: 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 computedConclusion¶
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.