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