Skip to content

Lazy Iteration

An array’s .map() builds a new array straight away. An iterator’s .map() does nothing yet: it returns a new iterator that will map values when something asks for them. That difference — eager versus lazy — is what lets iterators work on sequences that are large, or that never end.

An iterator is pull-based. Methods like filter, map, take and skip each wrap the iterator in front of them and return immediately. Only a consuming method — collect, count, fold, find, next and the rest — pulls values through the chain, one at a time:

let result = #[1, 2, 3, 4, 5, 6, 7, 8]
.iter()
.filter(fn(x) { return x % 2 == 0; }) // nothing runs yet
.map(fn(x) { return x * 10; }) // nothing runs yet
.collect(); // now every stage runs, value by value
print(result); // [20, 40, 60, 80]

No intermediate array is built between filter and map. Each value goes through every stage before the next one starts, and a consuming method that has its answer stops pulling: find stops at the first match, any at the first true.

An iterator is a position in a sequence, not the sequence itself. Once a value has been pulled, it is gone; an exhausted iterator returns NUL from next() and yields nothing more. Calling collect() twice on one iterator gives you the elements, then an empty array. To go over the data again, ask the source for a new iterator with .iter(). The source is unchanged, because iterating never modifies it.

A pattern is a query over an endless stream of cycles, so an iterator over its events never runs out. Laziness is what makes that workable: .take(8) caps the sequence, and everything after it sees exactly eight events.

let notes = [C4 E4 G4].iter()
.take(6)
.map(fn(e) { return e.note(); })
.collect();
print(notes); // [60, 64, 67, 60, 64, 67]

The flip side is that a consuming method on an uncapped pattern iterator — collect, count, last, fold — waits for an end that never comes. Put .take(n) first, every time.