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