Skip to content

Collections

Use the #[...] literal syntax or the Array() constructor.

let empty = #[];
let nums = #[1, 2, 3, 4, 5];
let mixed = #[1, "hello", true]; // mixed types allowed
let also_nums = Array(1, 2, 3); // same as #[1, 2, 3]

Elements are zero-indexed. Use bracket notation or the .get() method.

nums[0]; // 1
nums[2]; // 3
nums.get(4); // 5
FunctionDescriptionExample
length(arr)Number of elementslength(#[1,2,3])3
slice(arr, start, end)Sub-array from start to endslice(#[1,2,3,4], 1, 3)[2,3]
concat(a, b)Join two arraysconcat(#[1,2], #[3,4])[1,2,3,4]
range(n)Array of 0..n-1range(5)[0,1,2,3,4]
range(start, end)Array of start..end-1range(2, 5)[2,3,4]

Methods are called with dot syntax on the array value.

MethodDescription
.push(item)Append an item to the end
.pop()Remove and return the last item
let chars = #["a", "b", "c"];
chars.push("d"); // ["a", "b", "c", "d"]
let last = chars.pop(); // "d", chars is now ["a", "b", "c"]

These return new arrays; the original is unchanged.

MethodDescription
.get(index)Element at index
.length()Number of elements
.reverse()Reversed copy
.slice(start, end)Sub-array
.concat(other)Concatenated copy
.contains(value)true if value is present
let nums = #[1, 2, 3, 4, 5];
nums.reverse(); // [5, 4, 3, 2, 1]
nums.slice(1, 4); // [2, 3, 4]
nums.contains(3); // true
nums.contains(99); // false
let a = #[1, 2];
let b = #[3, 4];
a.concat(b); // [1, 2, 3, 4]
let matrix = #[
#[1, 2, 3],
#[4, 5, 6],
#[7, 8, 9]
];
matrix[0][1]; // 2
matrix[2][2]; // 9

Apply a function to every element and return a new array.

let doubled = #[1, 2, 3, 4, 5].map(fn(x) { return x * 2; });
// [2, 4, 6, 8, 10]

Keep only elements for which the function returns true.

let evens = #[1, 2, 3, 4, 5].filter(fn(x) { return x % 2 == 0; });
// [2, 4]

Accumulate a single value by applying a function to each element.

let sum = #[1, 2, 3, 4, 5].reduce(fn(acc, x) { return acc + x; }, 0);
// 15

Higher-order methods can be chained:

let result = #[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(fn(x) { return x % 2 == 0; })
.map(fn(x) { return x * x; })
.reduce(fn(acc, x) { return acc + x; }, 0);
// 220 (sum of squares of even numbers)

Assignment shares the same underlying array. Both variables see the same data.

let original = #[1, 2, 3];
let alias = original;
original.push(4);
PRINT original; // [1, 2, 3, 4]
PRINT alias; // [1, 2, 3, 4]

To create an independent copy, use .clone():

let original = #[1, 2, 3];
let copy = original.clone();
original.push(4);
PRINT original; // [1, 2, 3, 4]
PRINT copy; // [1, 2, 3]
let colors = #["red", "green", "blue"];
for color in colors {
PRINT color;
}

Collect values during iteration:

let shout = #[];
for color in colors {
shout.push(color + "!");
}
PRINT shout; // ["red!", "green!", "blue!"]

Call .iter() on an array to create a lazy iterator. Iterators process elements on demand rather than all at once, and can be chained into pipelines. Use .collect() to materialize the result back into an array.

let nums = #[1, 2, 3, 4, 5];
let it = nums.iter();
it.next(); // 1
it.next(); // 2

These return a new iterator without consuming elements. Nothing happens until you call an eager method.

MethodDescription
.take(n)Yield the first n elements
.skip(n)Skip the first n elements
.step_by(n)Yield every nth element
.enumerate()Yield [index, value] pairs
.zip(iter)Pair elements from two iterators
.chain(iter)Concatenate two iterators

These consume the iterator and return a value or array.

MethodReturnsDescription
.collect()ArrayGather all remaining elements into an array
.count()NumberCount remaining elements
.first()ValueFirst element (or NUL)
.last()ValueLast element (or NUL)
.next()ValueNext element (or NUL when exhausted)
.find(fn)ValueFirst element where fn returns true
.any(fn)Booleantrue if fn returns true for any element
.all(fn)Booleantrue if fn returns true for every element
.map(fn)ArrayApply fn to each element
.filter(fn)ArrayKeep elements where fn returns true
.fold(init, fn)ValueAccumulate with fn(acc, elem) starting from init
.flatten()ArrayFlatten one level of nested arrays
let nums = #[1, 2, 3, 4, 5];
nums.iter().take(3).collect(); // [1, 2, 3]
nums.iter().skip(2).collect(); // [3, 4, 5]
nums.iter().step_by(2).collect(); // [1, 3, 5]

Each element becomes an [index, value] pair.

let colors = #["red", "green", "blue"];
let pairs = colors.iter().enumerate().collect();
// [[0, "red"], [1, "green"], [2, "blue"]]

Pair elements from two iterators. Stops when the shorter one is exhausted.

let names = #["kick", "snare", "hat"];
let notes = #[36, 38, 42];
let pairs = names.iter().zip(notes.iter()).collect();
// [["kick", 36], ["snare", 38], ["hat", 42]]

Concatenate two iterators end-to-end.

let a = #[1, 2];
let b = #[3, 4, 5];
a.iter().chain(b.iter()).collect(); // [1, 2, 3, 4, 5]
let nums = #[1, 2, 3, 4, 5];
nums.iter().find(fn(x) { return x > 3; }); // 4
nums.iter().any(fn(x) { return x > 4; }); // true
nums.iter().all(fn(x) { return x > 0; }); // true

Flattens one level of nested structure. Non-array elements pass through unchanged.

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

Like reduce, but on iterators. Takes an initial value and an accumulator function.

let sum = #[1, 2, 3, 4, 5].iter().fold(0, fn(acc, x) { return acc + x; });
// 15

Convenience accessors for the endpoints of an iterator.

let nums = #[10, 20, 30];
nums.iter().first(); // 10
nums.iter().last(); // 30

Lazy methods build a pipeline; an eager method at the end executes it.

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

Dictionaries map string or number keys to any value. They are created with the #{...} literal syntax.

The #{} syntax uses a hash prefix to distinguish dictionaries from blocks. Keys and values are separated by colons, entries by commas.

let empty = #{};
let person = #{"name": "Alice", "age": 30, "active": true};

Trailing commas are allowed:

let config = #{
"volume": 80,
"tempo": 120,
"swing": 0.6,
};

Both keys and values are expressions evaluated at runtime:

let key = "tempo";
let val = 60 * 2;
let d = #{key: val};
d["tempo"]; // 120

Keys must be strings or numbers. The two types are distinct — 200 and "200" are different keys.

let d = #{200: "OK", "200": "two hundred"};
d[200]; // "OK"
d["200"]; // "two hundred"

Number keys are useful for MIDI mappings:

// MIDI note number to drum name
let drums = #{
36: "kick",
38: "snare",
42: "closed hat",
46: "open hat",
};
// CC number to parameter name
let cc_map = #{
1: "mod wheel",
7: "volume",
10: "pan",
64: "sustain",
};
person["name"]; // "Alice"
drums[36]; // "kick"

.get() returns NUL for missing keys instead of raising an error.

person.get("name"); // "Alice"
person.get("email"); // NUL
let config = #{"volume": 80, "tempo": 120};
config["volume"] = 100;
config.set("swing", 0.6);

Returns the removed value.

let old_tempo = config.remove("tempo");
PRINT old_tempo; // 120
config["volume"] += 5;
MethodDescription
.keys()Array of all keys
.values()Array of all values
.entries()Array of [key, value] pairs
.length()Number of key-value pairs
.contains_key(key)true if key exists
let d = #{"a": 1, "b": 2, "c": 3};
d.keys(); // ["a", "b", "c"]
d.values(); // [1, 2, 3]
d.entries(); // [["a", 1], ["b", 2], ["c", 3]]
d.length(); // 3
d.contains_key("a"); // true
d.contains_key("z"); // false

.merge(other) returns a new dictionary with entries from both. The original is unchanged. Keys in other overwrite keys in the receiver.

let defaults = #{"color": "blue", "size": 12, "bold": false};
let overrides = #{"color": "red", "bold": true};
let final_config = defaults.merge(overrides);
PRINT final_config; // {"color": "red", "size": 12, "bold": true}
PRINT defaults; // {"color": "blue", "size": 12, "bold": false}

Merge is ideal for layering configuration. Define sensible defaults, then let the caller override only what they need:

let track_defaults = #{
"channel": 1,
"velocity": 100,
"octave": 4,
"swing": 0.0,
};
let lead = track_defaults.merge(#{"channel": 3, "octave": 5});
let bass = track_defaults.merge(#{"channel": 2, "octave": 2, "velocity": 110});

Merges can be chained to layer multiple sources. Later values win:

let base = #{"a": 1, "b": 2, "c": 3};
let layer1 = #{"b": 20};
let layer2 = #{"c": 30, "d": 40};
let result = base.merge(layer1).merge(layer2);
// {"a": 1, "b": 20, "c": 30, "d": 40}

Apply a function to every entry and return a new dictionary with the same keys and transformed values.

let scores = #{"alice": 85, "bob": 92, "carol": 78};
let doubled = scores.map(fn(k, v) { return v * 2; });
// {"alice": 170, "bob": 184, "carol": 156}

Keep only entries for which the function returns true.

let top = scores.filter(fn(k, v) { return v > 90; });
// {"bob": 92}

Accumulate a single value across all entries.

let total = scores.reduce(fn(acc, k, v) { return acc + v; }, 0);
// 255

Assignment shares the same underlying dictionary. Both variables see the same data.

let original = #{"a": 1, "b": 2};
let alias = original;
original["c"] = 3;
PRINT original; // {"a": 1, "b": 2, "c": 3}
PRINT alias; // {"a": 1, "b": 2, "c": 3}

To create an independent copy, use .clone():

let original = #{"a": 1, "b": 2};
let copy = original.clone();
original["c"] = 3;
PRINT original; // {"a": 1, "b": 2, "c": 3}
PRINT copy; // {"a": 1, "b": 2}

for-in on a dictionary yields keys. Use bracket notation to access the corresponding value.

let colors = #{"r": 255, "g": 128, "b": 0};
for k in colors {
PRINT f"{k} => {colors[k]}";
}

Use .entries() to iterate over [key, value] pairs directly:

let cc_map = #{1: "mod wheel", 7: "volume", 10: "pan"};
for entry in cc_map.entries() {
let cc = entry[0];
let name = entry[1];
PRINT f"CC {cc}: {name}";
}
let source = #{"a": 1, "b": 2, "c": 3};
let doubled = #{};
for k in source {
doubled[k] = source[k] * 2;
}
// {"a": 2, "b": 4, "c": 6}

Call .iter() on a dictionary to create a lazy iterator over its keys. Iterators process elements on demand rather than all at once, and can be chained into pipelines. Use .collect() to materialize the result back into an array.

let d = #{"kick": 36, "snare": 38, "hat": 42};
let it = d.iter();
it.next(); // "kick"
it.next(); // "snare"

All standard iterator methods are available — take, skip, map, filter, collect, find, any, all, fold, and more. See the Arrays iterator section for the full method reference.

let instruments = #{
"piano": 1,
"bass": 2,
"drums": 10,
"strings": 3,
"pad": 4,
};
// Get the first 3 instrument names
let first_three = instruments.iter().take(3).collect();
let cc_labels = #{1: "mod", 7: "vol", 10: "pan", 64: "sustain", 74: "cutoff"};
// Find CC numbers above 10
let high_ccs = cc_labels.iter()
.filter(fn(k) { return k > 10; })
.collect();
// [64, 74]

Dictionaries can contain arrays and vice versa.

// Dict containing an array
let playlist = #{
"name": "Focus",
"tracks": #["Ambient 1", "Ambient 2", "Ambient 3"]
};
playlist["tracks"][0]; // "Ambient 1"
// Array of dicts
let instruments = #[
#{"name": "Piano", "channel": 1},
#{"name": "Bass", "channel": 2}
];
instruments[1]["name"]; // "Bass"

Nested dicts work well for instrument configurations:

let kit = #{
"kick": #{
"channel": 10,
"note": 36,
"velocity": #[100, 110, 120, 127],
},
"snare": #{
"channel": 10,
"note": 38,
"velocity": #[80, 100, 110, 120],
},
};
kit["kick"]["note"]; // 36
kit["snare"]["velocity"][2]; // 110