Skip to content

Collections & Iterators

The methods of each type are in the generated reference: Array, Dict, Tuple and Iterator. Arrays, dicts and class instances are shared on assignment; see Values & Sharing.

TypeLiteralSizeMutableElements
Array#[a, b, c]Grows and shrinksYesAny, mixed
Dict#{key: value, …}Grows and shrinksYesString or Number keys, any values
Tuple(a, b, …)Fixed, at least 2NoAny, mixed

Trailing commas are allowed in all three literals, and all three nest.

let nums = #[1, 2, 3, 4, 5];
assert nums[0] == 1; // zero-based
assert nums.get(4) == 5; // same as indexing
nums[0] = 10; // index assignment
nums.push(6); // push and pop mutate in place
assert nums.pop() == 6;
let matrix = #[#[1, 2], #[3, 4]];
assert matrix[1][0] == 3;
print(nums);

.push() and .pop() change the array; every other method returns a new value. An array is played as a pattern with Sequence(array).

Keys are Strings or Numbers, and the two are distinct: 200 and "200" are different keys. A dict has no order — .keys(), .values() and .entries() return entries in any order.

let cc = #{"cutoff": 74, "reso": 71, 7: "volume"};
assert cc["cutoff"] == 74; // a missing key is an error
assert cc.get("drive") == NUL; // .get returns NUL instead
cc["cutoff"] = 20; // index assignment adds or replaces
cc["cutoff"] += 1;
cc.set("drive", 0.3);
assert cc.remove("reso") == 71; // .remove returns the removed value
assert cc.contains_key(7);
print(cc);

.merge(other) returns a new dict in which other’s keys win. The dict callbacks of .map(), .filter() and .reduce() receive the key and the value: fn(k, v), fn(acc, k, v).

A tuple is a fixed group of two or more values; (1) is a parenthesised expression, not a tuple. It cannot be changed. Index it with t[i]; compare it with == and !=, element by element (tuples of different lengths are never equal). Unpack it with a destructuring let or a match tuple pattern.

let point = (3, 4);
let mixed = (1, "hello", C4);
assert mixed[2] == C4;
assert mixed.length() == 3;
assert (1, 2) == (1, 2);
assert (1, 2) != (1, 2, 3);
let box = ((0, 0), (640, 480));
assert box[1][0] == 640;
print(point);

A DSP block’s stereo return (out_l, out_r) is a tuple.

An iterator is a lazy, single-use sequence. Get one with .iter():

SourceYieldsEnds
Array, TupleEach elementAfter the last
StringEach character, as a one-character StringAfter the last
DictEach key, in unspecified orderAfter the last
PatternEach event, one per stepNever

The Iterator reference groups its methods:

  • Advancing and transforming methods (take, skip, step_by, enumerate, map, filter, zip, chain) return a new iterator and do no work yet. enumerate and zip yield [a, b] arrays; zip stops when either side ends.
  • Consuming methods (next, collect, count, first, last, find, any, all, fold, flatten) pull values through and return a result. next() returns NUL once the iterator is exhausted.
let it = #[10, 20].iter();
assert it.next() == 10;
assert it.next() == 20;
assert it.next() == NUL; // exhausted, and stays exhausted
let evens = #[1, 2, 3, 4, 5, 6].iter()
.filter(fn(x) { return x % 2 == 0; }) // lazy
.map(fn(x) { return x * 10; }) // lazy
.collect(); // runs the pipeline
print(evens); // [20, 40, 60]
let notes = [C4 E4 G4].iter().take(3).map(fn(e) { return e.note(); }).collect();
print(notes); // [60, 64, 67]

A pattern iterator never ends, so a consuming method on it without .take(n) first never returns:

[C4 D4 E4].iter().collect(); // runs forever

Array .map() and .filter() return arrays; the same methods on an iterator return iterators. for … in accepts an iterator directly.