Dictionaries
Dictionaries map string or number keys to any value. They are created with the #{...} literal syntax.
Creating dictionaries
Section titled “Creating dictionaries”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"]; // 120Key types
Section titled “Key types”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 namelet drums = #{ 36: "kick", 38: "snare", 42: "closed hat", 46: "open hat",};
// CC number to parameter namelet cc_map = #{ 1: "mod wheel", 7: "volume", 10: "pan", 64: "sustain",};Accessing values
Section titled “Accessing values”Bracket notation
Section titled “Bracket notation”person["name"]; // "Alice"drums[36]; // "kick".get() (safe access)
Section titled “.get() (safe access)”.get() returns NUL for missing keys instead of raising an error.
person.get("name"); // "Alice"person.get("email"); // NULMutating
Section titled “Mutating”Index assignment
Section titled “Index assignment”let config = #{"volume": 80, "tempo": 120};config["volume"] = 100;.set(key, value)
Section titled “.set(key, value)”config.set("swing", 0.6);.remove(key)
Section titled “.remove(key)”Returns the removed value.
let old_tempo = config.remove("tempo");PRINT old_tempo; // 120Compound assignment
Section titled “Compound assignment”config["volume"] += 5;Inspection methods
Section titled “Inspection methods”| Method | Description |
|---|---|
.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(); // 3d.contains_key("a"); // trued.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}Defaults/overrides pattern
Section titled “Defaults/overrides pattern”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});Chaining merges
Section titled “Chaining merges”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}Higher-order methods
Section titled “Higher-order methods”.map(fn(k, v))
Section titled “.map(fn(k, v))”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}.filter(fn(k, v))
Section titled “.filter(fn(k, v))”Keep only entries for which the function returns true.
let top = scores.filter(fn(k, v) { return v > 90; });// {"bob": 92}.reduce(fn(acc, k, v), init)
Section titled “.reduce(fn(acc, k, v), init)”Accumulate a single value across all entries.
let total = scores.reduce(fn(acc, k, v) { return acc + v; }, 0);// 255Copy-on-assignment
Section titled “Copy-on-assignment”Assignment creates an independent deep copy. Mutating one dictionary does not affect the other.
let original = #{"a": 1, "b": 2};let copy = original;original["c"] = 3;
PRINT original; // {"a": 1, "b": 2, "c": 3}PRINT copy; // {"a": 1, "b": 2}To share state between variables, use References.
Iteration
Section titled “Iteration”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]}";}Iterating key-value pairs
Section titled “Iterating key-value pairs”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}";}Building a new dict from iteration
Section titled “Building a new dict from iteration”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}Iterators
Section titled “Iterators”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.
Chaining lazy methods
Section titled “Chaining lazy methods”let instruments = #{ "piano": 1, "bass": 2, "drums": 10, "strings": 3, "pad": 4,};
// Get the first 3 instrument nameslet first_three = instruments.iter().take(3).collect();Filtering keys
Section titled “Filtering keys”let cc_labels = #{1: "mod", 7: "vol", 10: "pan", 64: "sustain", 74: "cutoff"};
// Find CC numbers above 10let high_ccs = cc_labels.iter() .filter(fn(k) { return k > 10; }) .collect();// [64, 74]Nesting
Section titled “Nesting”Dictionaries can contain arrays and vice versa.
// Dict containing an arraylet playlist = #{ "name": "Focus", "tracks": #["Ambient 1", "Ambient 2", "Ambient 3"]};playlist["tracks"][0]; // "Ambient 1"
// Array of dictslet 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"]; // 36kit["snare"]["velocity"][2]; // 110