What types can be used as Dict keys?
← Advanced Type System · Ref: Q260
Dict keys must support equality and hashing, giving O(1) lookups. Internally, Dict is backed by a Java LinkedHashMap whose key matching uses the key type's '==' (equality) and '#?' (hashcode) operators — the compiler bridges these to Java equals()/hashCode() for generated user types. Two keys match when '==' returns true and they share the same '#?' hash. The '<=>' (comparison) operator is NOT used for Dict key lookup at runtime, but it IS still required for the 'default operator' set (default '==' depends on '<=>' being defaulted first).
KEY REQUIREMENTS
A type used as a Dict key must implement:
operator == (equality) - used for Dict key matching at runtime; required operator #? (hashcode) - used for Dict O(1) hashing at runtime; required operator <=> (comparison) - prerequisite for the 'default operator' set and for sorting; not used for Dict lookup itself, returns Integer operator $ (string) - for string representation operator ? (isSet) - for validity checking
BUILT-IN KEY TYPES
All these types work as Dict keys out of the box:
String, Integer, Float, Character, Boolean Date, DateTime, Duration, Millisecond Enumerations (all enum values support comparison)
CUSTOM KEY TYPES
Use records with default operators for the simplest approach:
PlayerName
firstName as String: String()
lastName as String: String()
default PlayerName()
PlayerName() ...
default operator <=>
default operator #?
default operator ==
default operator $
default operator ?
The 'default' keyword auto-generates these operators based on field declarations.
You can also implement operators manually for custom comparison logic.
DICT INTERNALS
Dict uses LinkedHashMap internally, which means insertion order is preserved. When you iterate over a Dict, entries come out in the order they were added. Key equality is determined by the '==' operator (bridged to the generated equals()/hashCode()), and keys are hashed via '#?', giving O(1) contains/getOrDefault/remove via the native LinkedHashMap rather than a linear scan.
ENUMERATION KEYS
Enumerations make excellent Dict keys because they have built-in comparison and hashcode:
scores <- Dict() of (Direction, Integer) scores += DictEntry(Direction.North, 10)
See Q46 for Dict basics. See Q90 for Dict operations. See Q129 for handling missing Dict keys. See Q167 for Dict iteration patterns.
Example
defines module qa.advancedtypes.dictkeytype defines record PlayerName firstName as String: String() lastName as String: String() default PlayerName() PlayerName() -> fn as String ln as String firstName: fn lastName: ln default operator <=> default operator #? default operator == default operator $ default operator ? defines program DictKeyTypesDemo() stdout <- Stdout() // === BUILT-IN TYPES AS KEYS === // String keys (most common) ages <- {"Alice": 30, "Bob": 25} stdout.println(`String keys: ${ages}`) // Integer keys labels <- {1: "first", 2: "second", 3: "third"} stdout.println(`Integer keys: ${labels}`) // === CUSTOM RECORD AS KEY === scores <- Dict() of (PlayerName, Integer) scores += DictEntry(PlayerName("Alice", "Smith"), 95) scores += DictEntry(PlayerName("Bob", "Jones"), 87) scores += DictEntry(PlayerName("Charlie", "Brown"), 72) // Lookup by key (matched via == and #? hashing) aliceScore <- scores.getOrDefault(PlayerName("Alice", "Smith"), 0) stdout.println(`Alice score: ${aliceScore}`) // Missing key returns default unknownScore <- scores.getOrDefault(PlayerName("Dave", "Wilson"), 0) stdout.println(`Unknown score: ${unknownScore}`) // === ITERATION PRESERVES INSERTION ORDER === for entry in scores stdout.println(`${entry.key()}: ${entry.value()}`) // === LENGTH AND EMPTY CHECK === stdout.println(`Score count: ${length scores}`) stdout.println(`Scores empty: ${scores is empty}`)
Common mistakes
E07180 — Removing 'default operator <=>' from PlayerName means the record has no comparison operator. This cascades: 'default operator ==' depends on '<=>' and fails with E07180. Without comparison and equality, the type cannot function as a Dict key. The <=> operator is foundational — other operators depend on it and Dict key lookup requires it. See ek9 -h E07180 for details.
Incorrect:
//removed compare
Correct:
default operator <=>
Other ways to ask this
- What are the requirements for Dict key types?
- Can I use custom classes as Dict keys in EK9?
- How does Dict find keys internally?
- Do Dict keys need hashcode and comparison operators?
Coming from another language?
Java: HashMap requires equals() and hashCode() on keys, LinkedHashMap preserves insertion order. Python: dict keys must be hashable (__hash__ and __eq__). Rust: HashMap keys need Hash + Eq traits. Go: map keys must be comparable (==). C#: Dictionary keys need GetHashCode() and Equals(). Kotlin: same as Java (equals/hashCode). EK9: Dict keys match by '==' (equality) and '#?' (hashcode) via a backing LinkedHashMap (insertion order preserved); records use 'default operator' to auto-generate the full set, which also includes '<=>' (still the default-operator prerequisite).
Keywords: custom, type, record, order, advanced, operator, dict, enumeration, requirement, hashcode, type-system, comparison, key, insertion