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 the record's sameness: '==' (and '<>') are generated from '<=>', never written or requested separately.
KEY REQUIREMENTS
A type used as a Dict key must implement:
operator == (equality) - used for Dict key matching at runtime; generated from the type's sameness operator #? (hashcode) - used for Dict O(1) hashing at runtime; required, and hashes what the sameness compares operator <=> (comparison) - the sameness of an ordered type (use '==' as the sameness for an equatable-only type); '==' is generated from it and it is used 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 ?
The 'default' keyword auto-generates these operators based on field declarations; '==' and '<>' are always generated from the '<=>' sameness, so writing 'default operator ==' beside it is an error (E07181).
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 ? 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
E07184 — Removing 'default operator <=>' from PlayerName leaves the record with no sameness ('<=>' or '=='), so 'default operator #?' has nothing to agree with and fails with E07184. Without sameness there is no generated '==', and the type cannot function as a Dict key. Declare the sameness once ('<=>' for an ordered type, '==' for an equatable-only one) and let the compiler generate the rest. See ek9 -h E07184 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, where '<=>' is the sameness and '==' is generated from it.
Keywords: dict, operator, advanced, order, record, type, custom, insertion, key, comparison, type-system, hashcode, requirement, enumeration