Skip to content

Iterators

An iterator is a lazy, pull-based view over a sequence. Instead of building intermediate arrays at every step, you chain transformations into a pipeline and pull values through it one at a time — which also means you can work with sequences that are huge, or even infinite, like a pattern. Call .iter() on an array, string, dictionary, or pattern to get one.

let result = #[1, 2, 3, 4, 5, 6, 7, 8]
.iter()
.filter(fn(x) { return x % 2 == 0; }) // lazy
.map(fn(x) { return x * 10; }) // lazy
.collect(); // eager — runs the pipeline
PRINT result; // [20, 40, 60, 80]

An iterator is single-use. .next() advances it one step and returns the current element; once it is exhausted, .next() returns NUL forever:

let it = #[10, 20, 30].iter();
PRINT it.next(); // 10
PRINT it.next(); // 20
PRINT it.next(); // 30
PRINT it.next(); // NUL — exhausted

To iterate the same data again, make a fresh iterator from the source:

let arr = #[1, 2, 3];
let sum = arr.iter().fold(0, fn(acc, x) { return acc + x; });
let doubled = arr.iter().map(fn(x) { return x * 2; }).collect(); // new iterator
PRINT sum; // 6
PRINT doubled; // [2, 4, 6]
let it = #[1, 2, 3, 4, 5].iter();
PRINT it.collect();

String iterators yield individual characters:

PRINT "hello".iter().collect(); // [h, e, l, l, o]

Dictionary iterators yield keys (in unspecified order); index back in for values:

let d = #{"root": C4, "third": E4, "fifth": G4};
for key in d.iter() {
PRINT key + ": " + d[key];
}

A pattern iterator yields events — and it is infinite, repeating forever. Always limit it with .take(n) before any eager method:

let events = [C4 D4 E4].iter().take(6).collect();
PRINT events.length(); // 6
[C4 D4 E4].iter().collect(); // WRONG — runs forever

This is the distinction that makes iterators work. Lazy methods return a new iterator and do no work yet; eager methods consume elements and produce a result. Build a pipeline of lazy methods, then terminate it with one eager method.

MethodKindReturns
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

take, skip, and step_by slice a sequence; chain them freely:

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

map and filter chain in the same pipeline since both are lazy:

let result = #[1, 2, 3, 4, 5].iter()
.filter(fn(x) { return x % 2 == 1; })
.map(fn(x) { return x * 10; })
.collect();
PRINT result; // [10, 30, 50]
PRINT #["a", "b", "c"].iter().enumerate().collect();
// [[0, a], [1, b], [2, c]]
let names = #["kick", "snare", "hat"];
let notes = #[36, 38, 42];
PRINT names.iter().zip(notes.iter()).collect();
// [[kick, 36], [snare, 38], [hat, 42]]
PRINT #[1, 2].iter().chain(#[3, 4, 5].iter()).collect();
// [1, 2, 3, 4, 5]

zip stops as soon as either side is exhausted.

Eager methods turn a pipeline into a single value:

let nums = #[1, 2, 3, 4, 5];
PRINT nums.iter().find(fn(x) { return x > 3; }); // 4
PRINT nums.iter().any(fn(x) { return x > 4; }); // true
PRINT nums.iter().all(fn(x) { return x > 0; }); // true
PRINT nums.iter().fold(0, fn(acc, x) { return acc + x; }); // 15

fold is reduce for iterators: an initial value plus an accumulator function. flatten collapses one level of nesting:

PRINT #[#[1, 2], #[3], #[4, 5]].iter().flatten(); // [1, 2, 3, 4, 5]

When you iterate a pattern, each element is an event with these accessors:

MethodReturnsDescription
.note()Number (0–127)MIDI note number
.velocity()Number (0–127)Velocity (getter)
.velocity(v)EventNew event with velocity v (0–127)
.channel()NumberMIDI channel
.start()NumberStart time in cycles
.duration()NumberDuration in cycles
.end()Numberstart + duration
.transpose(n)EventNew event transposed by n semitones

Extract note numbers from a melody — remember to .take() first:

let melody = [C4 D4 E4 F4];
let notes = melody.iter()
.take(4)
.map(fn(e) { return e.note(); })
.collect();
PRINT notes; // [60, 62, 64, 65]

for … in accepts iterators directly:

for pair in #["a", "b", "c"].iter().enumerate() {
PRINT pair[0] + ": " + pair[1];
}
for event in [C4 D4].iter().take(4) { // always limit pattern iterators
PRINT event.note();
}

You can also loop straight over an array without .iter() — use the iterator form when you want to take, skip, or otherwise shape the sequence first.

  • Single-use. A consumed iterator yields nothing more. it.collect() twice returns the full array, then an empty one. Make a new iterator from the source for a second pass.
  • Infinite pattern iterators. collect(), count(), last(), and fold() all consume the whole iterator — on a pattern that never ends, they hang. Put .take(n) first, every time.
// safe — limited before the eager call
PRINT [C4 D4].iter().take(8).collect().length(); // 8
let melody = [C4 E4 G4 B4 D5 C5];
let notes = melody.iter().take(6).map(fn(e) { return e.note(); }).collect();
let highest = notes.iter().fold(0, fn(acc, n) { if n > acc { return n; } return acc; });
let lowest = notes.iter().fold(127, fn(acc, n) { if n < acc { return n; } return acc; });
PRINT "range: " + (highest - lowest) + " semitones"; // 14

Thin a scale down to every other note, then play the result as a Sequence:

let lead = MidiTrack(1);
let scale = [C4 D4 E4 F4 G4 A4 B4 C5];
let thinned = scale.iter()
.take(8)
.step_by(2)
.map(fn(e) { return e.note(); })
.collect();
// [60, 64, 67, 72] — C4, E4, G4, C5
lead << Sequence(thinned);
PLAY;