Skip to content

Dictionaries

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 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.

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