Skip to content

Iterators

This content is for v0.7. Switch to the latest version for up-to-date documentation.

Iterators provide lazy, pull-based access to sequences. They let you process elements one at a time without loading everything into memory.

An iterator is a single-use sequence. Once an element is consumed, it cannot be retrieved again. This makes iterators efficient for processing large or infinite sequences.

Calling .next() advances the iterator by one position and returns the current element. When the iterator is exhausted, .next() returns nul:

CallPositionReturnsRemaining
start10, 20, 30
.next()11020, 30
.next()22030
.next()330(empty)
.next()nul(exhausted)
let it = #[10, 20, 30].iter();
it.next() // 10
it.next() // 20
it.next() // 30
it.next() // nul
let it = #[1, 2, 3, 4, 5].iter();

Array iterators are finite — they end when the array is exhausted.

let chars = "hello".iter();

String iterators yield individual characters.

let d = %{ "a": 1, "b": 2, "c": 3 };
let keys = d.iter();

Dict iterators yield keys only. To access values, index back into the dict:

let d = %{ "root": C4, "third": E4, "fifth": G4 };
for key in d.iter() {
PRINT key + ": " + d[key];
}
let events = [C4 D4 E4].iter();
// CORRECT: limit with take()
let events = [C4 D4 E4].iter().take(6).collect();
// WRONG: will run forever!
// let events = [C4 D4 E4].iter().collect();

Iterator methods fall into two categories: lazy methods return a new iterator (no work happens yet), and eager methods consume elements immediately and return a result.

MethodBehaviorReturns
take(n)LazyIterator
skip(n)LazyIterator
step_by(n)LazyIterator
enumerate()LazyIterator (of [index, value])
zip(other)LazyIterator (of [a, b])
chain(other)LazyIterator
map(fn)LazyIterator
filter(fn)LazyIterator
next()EagerValue or nul
first()EagerValue or nul
last()EagerValue or nul
count()EagerNumber
collect()EagerArray
find(fn)EagerValue or nul
any(fn)EagerBoolean
all(fn)EagerBoolean
fold(init, fn)EagerValue
flatten()EagerArray

The pipeline model: chain lazy operations to set up the transformation, then terminate with an eager operation to produce a result.

#[1, 2, 3, 4, 5, 6, 7, 8].iter()
.skip(2) // lazy — skip first 2
.take(4) // lazy — limit to 4
.collect() // eager — produce Array(3, 4, 5, 6)

These methods consume elements and return single values:

let it = #[1, 2, 3, 4, 5].iter();
it.next() // Returns next element or nul if exhausted
it.first() // Returns first element (same as next())
it.last() // Returns last element (WARNING: hangs on infinite iterators!)
it.count() // Count elements (consumes iterator)

These methods return new iterators:

let it = #[1, 2, 3, 4, 5].iter();
it.take(3) // First 3 elements
it.skip(2) // Skip first 2 elements
it.step_by(2) // Every 2nd element
it.enumerate() // Yields [index, value] pairs

Transforming methods chain naturally:

let result = #[1, 2, 3, 4, 5, 6, 7, 8].iter()
.skip(1)
.step_by(2)
.take(3)
.collect();
// Array(2, 4, 6)

Convert an iterator back to an array:

let arr = #[1, 2, 3].iter().take(2).collect();
// Array(1, 2)

These methods take functions as arguments.

Note: map and filter are lazy — they return iterators. Append .collect() to get an array.

let it = #[1, 2, 3, 4, 5].iter();
// Transform each element (lazy — returns iterator, collect to get array)
it.map(fn(x) { return x * 2; }).collect() // Array(2, 4, 6, 8, 10)
// Keep matching elements (lazy — returns iterator, collect to get array)
it.filter(fn(x) { return x > 2; }).collect() // Array(3, 4, 5)
// Find first match (returns value or nul)
it.find(fn(x) { return x > 3; }) // 4
// Check if any/all elements match
it.any(fn(x) { return x > 4; }) // true
it.all(fn(x) { return x > 0; }) // true
// Reduce to single value
it.fold(0, fn(acc, x) { return acc + x; }) // 15

Since map() and filter() are lazy, they chain directly — no intermediate .collect() or .iter() needed:

// Filter then map in one pipeline
let result = #[1, 2, 3, 4, 5].iter()
.filter(fn(x) { return x % 2 == 1; }) // lazy — Iterator
.map(fn(x) { return x * 10; }) // lazy — Iterator
.collect(); // Array(10, 30, 50)

Combine two iterators into pairs. Stops when either iterator is exhausted:

let a = #[1, 2, 3].iter();
let b = #[4, 5, 6].iter();
let zipped = a.zip(b).collect();
// [[1, 4], [2, 5], [3, 6]]

Concatenate iterators end-to-end:

let a = #[1, 2].iter();
let b = #[3, 4].iter();
let chained = a.chain(b).collect();
// Array(1, 2, 3, 4)

Flatten nested arrays (eager, returns array):

let nested = #[#[1, 2], #[3, 4]].iter();
let flat = nested.flatten();
// Array(1, 2, 3, 4)

Common patterns for combining iterator methods:

PatternMethodsEffectUse case
Extract-transformtake + mapPull N elements and transformGrabbing a fixed slice of data
Windowingskip + takeSelect a range from the middleAccessing a subrange
Samplingstep_byEvery Nth elementThinning a sequence
PairingzipParallel iteration over two sourcesCombining notes + velocities
AccumulationfoldReduce to a single valueComputing statistics
Searchfind / any / allQuery the sequenceLooking for specific elements
ExtendchainConcatenate multiple sourcesBuilding longer sequences

Step-by-step breakdown of a chained pipeline:

let result = #[10, 20, 30, 40, 50, 60, 70, 80].iter()
.skip(2) // skip 10, 20 → [30, 40, 50, 60, 70, 80]
.take(4) // take 4 → [30, 40, 50, 60]
.collect(); // Array(30, 40, 50, 60)
StepOperationRemaining sequence
Start.iter()10, 20, 30, 40, 50, 60, 70, 80
1.skip(2)30, 40, 50, 60, 70, 80
2.take(4)30, 40, 50, 60
3.collect()Array(30, 40, 50, 60)

Pattern iterators yield events that represent individual notes or samples in the pattern.

Events from pattern iteration support these methods:

MethodReturnsDescription
.note()Number (0–127)MIDI note number
.velocity()Number (0–127)Note velocity (getter)
.velocity(v)EventNew event with velocity set to v (setter, clamped 0–127)
.channel()NumberMIDI channel
.start()NumberStart time in cycles
.duration()NumberDuration in cycles
.end()NumberEnd time (start + duration)
.transpose(n)EventNew event transposed by n semitones
// Always use take() for patterns!
for event in [C4 D4 E4].iter().take(6) {
PRINT event.note();
}

Without .take(), this loop would run forever.

let melody = [C4 D4 E4 F4];
// Get all note numbers
let notes = melody.iter()
.take(4)
.map(fn(e) { return e.note(); })
.collect();
// Array(60, 62, 64, 65)
let pattern = [C4 E4 G4 B4];
// Extract events and compute the average MIDI note
let events = pattern.iter().take(4);
let avg = events.fold(
#[0, 0],
fn(acc, e) {
return #[acc[0] + e.note(), acc[1] + 1];
}
);
let average_note = avg[0] / avg[1];
PRINT "Average MIDI note: " + average_note;
// Average MIDI note: 65.25

Iterators work with for-in loops:

// Array iteration
for x in #[1, 2, 3].iter() {
PRINT x;
}
// String iteration
for char in "hello".iter() {
PRINT char;
}
// Pattern iteration (with limit!)
for event in [C4 D4].iter().take(4) {
PRINT event.note();
}
// Enumerate gives index and value
for pair in #["a", "b", "c"].iter().enumerate() {
PRINT pair[0] + ": " + pair[1];
}

You can also iterate directly over arrays without .iter():

for x in #[1, 2, 3] {
PRINT x;
}

Iterators are single-use. After all elements are consumed, every call to .next() returns nul:

let it = #[1, 2].iter();
it.next() // 1
it.next() // 2
it.next() // nul — exhausted
it.next() // nul — still exhausted

To iterate again, create a new iterator from the source:

let arr = #[1, 2, 3];
// First pass
let sum = arr.iter().fold(0, fn(acc, x) { return acc + x; });
// Second pass — new iterator from the same array
let doubled = arr.iter().map(fn(x) { return x * 2; }).collect();

Pattern iterators never end. Any eager method that consumes the entire iterator will hang:

// These will all hang on pattern iterators:
// [C4 D4].iter().collect()
// [C4 D4].iter().count()
// [C4 D4].iter().last()
// [C4 D4].iter().fold(...)
// Always limit first:
[C4 D4].iter().take(8).collect() // safe

Passing a consumed iterator to a method returns an empty result:

let it = #[1, 2, 3].iter();
let first_pass = it.collect(); // Array(1, 2, 3)
let second_pass = it.collect(); // Array() — empty, already consumed
.iter() pipelinefor loop on arrayPattern .map()
Works onArrays, strings, dicts, patternsArrays, iteratorsPatterns
Lazy chainingYes (take, skip, step_by, etc.)NoNo
ReturnsArray or value (after eager op)Nothing (side effects only)Pattern
Best forData extraction, analysis, transformation pipelinesSide effects (PRINT, assignments)Transforming pattern output for playback
Infinite-safeYes, with .take()Yes, with .take()Yes (patterns handle cycles internally)

Extract note data from a pattern and compute statistics:

let melody = [C4 E4 G4 B4 D5 C5];
// Get note numbers
let notes = melody.iter()
.take(6)
.map(fn(e) { return e.note(); })
.collect();
// Array(60, 64, 67, 71, 74, 72)
// Find the highest note
let highest = notes.iter()
.fold(0, fn(acc, n) {
if n > acc { return n; }
return acc;
});
PRINT "Highest: " + highest; // 74 (D5)
// Find the lowest note
let lowest = notes.iter()
.fold(127, fn(acc, n) {
if n < acc { return n; }
return acc;
});
PRINT "Lowest: " + lowest; // 60 (C4)
PRINT "Range: " + (highest - lowest) + " semitones"; // 14

Zip notes and velocities together, then use the data to construct a new pattern:

let track = MidiTrack(1);
let notes = #[C4, E4, G4, B4];
let velocities = #[100, 80, 90, 70];
// Pair notes with velocities
let pairs = notes.iter()
.zip(velocities.iter())
.collect();
// [[C4, 100], [E4, 80], [G4, 90], [B4, 70]]
// Use the pairs to build a velocity-shaped arpeggio
class VelocityArp {
let pairs;
fn new(pairs) { this.pairs = pairs; }
fn query(cycle) {
let pair = this.pairs[cycle % 4];
return pair[0].velocity(pair[1]);
}
}
track << VelocityArp(pairs);
PLAY;

Use step_by to thin a pattern, keeping every other note:

let track = MidiTrack(1);
let scale = [C4 D4 E4 F4 G4 A4 B4 C5];
// Take every other note from the scale
let thinned = scale.iter()
.take(8)
.step_by(2)
.map(fn(e) { return e.note(); })
.collect();
// Array(60, 64, 67, 72) — C4, E4, G4, C5
PRINT thinned;