How does dispatcher resolution work across a class hierarchy?

← Dispatcher Validation · Ref: Q620

EK9 uses a cost-based model to resolve dispatchers. When a dispatcher is called, the compiler has already validated the hierarchy at compile time. At runtime, the most specific (lowest cost) handler is selected.

COST MODEL

Exact type match: cost 0.00. Each class inheritance step: cost 0.05 per level. Each trait implementation step: cost 0.10 per level. The handler with the lowest total cost wins.

FALLBACK TO ENTRY

If no specific handler matches, the dispatcher entry method executes. This is the catch-all fallback for types without dedicated handlers.

INHERITANCE DEPTH MATTERS

A handler for a direct parent (cost 0.05) beats a handler for a grandparent (cost 0.10). More specific handlers always win over less specific ones.

COMPILE-TIME VALIDATION

All type hierarchy checks (E05210), return type checks (E05220), purity checks (E05170), and access checks (E05180) happen at compile time. Runtime dispatch only selects the handler; correctness is guaranteed by the compiler.

TRAIT VS CLASS COST

Trait implementation costs more than class inheritance (0.10 vs 0.05). This reflects the structural reality that class inheritance is a stronger type relationship than trait implementation.

See Q60 for dispatcher fundamentals. See Q612 for purity matching. See Q614 for type hierarchy validation. See Q255 for cost-based method resolution.

Example

defines module qa.dispatchervalidation.rules

  defines class

    //Three-level hierarchy to demonstrate cost-based resolution
    Entity as abstract
      kind() as pure abstract
        <- rtn as String?
      default operator ?

    LivingEntity extends Entity as open
      override kind() as pure
        <- rtn as String: "living"
      default operator ?

    Person extends LivingEntity
      override kind() as pure
        <- rtn as String: "person"
      default operator ?

    //Dispatcher with handlers at different hierarchy depths
    EntityProcessor
      process() as dispatcher
        -> entity as Entity
        <- rtn as String: "Generic: " + entity.kind()

      //Cost 0.05 from LivingEntity (one step from Entity)
      process()
        -> entity as LivingEntity
        <- rtn as String: "Living: " + entity.kind()

      //Cost 0.10 from Person (two steps from Entity)
      //But cost 0.00 for exact Person match
      process()
        -> entity as Person
        <- rtn as String: "Person: " + entity.kind()

  defines function

    testCostBasedResolution()
      processor <- EntityProcessor()

      person <- Person()
      //Person matches Person handler at cost 0.00 (exact match)
      require processor.process(person) == "Person: person"

Common mistakes

E50060 — The dispatcher entry takes Entity. All handler parameter types must be within the Entity hierarchy. String is unrelated and would never match. See ek9 -h E50060 for details.

Incorrect:

process()
        -> entity as String

Correct:

process()
        -> entity as Person

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

require processor.process(person).toUpperCase() == "Person: person"

Correct:

require processor.process(person) == "Person: person"
Other ways to ask this
  • What are the dispatch resolution rules in EK9?
  • How does EK9 choose which handler to call?
  • What is the cost model for dispatcher resolution?

Coming from another language?

Java: virtual dispatch via vtable (single dispatch, O(1)). C++: virtual function tables with dynamic_cast for type checking. Python: MRO-based method resolution. Kotlin: no runtime dispatch, compile-time overload resolution. Julia: multiple dispatch with method cache. EK9: cost-based multiple dispatch with compile-time hierarchy validation.

Keywords: ambiguity, class, visitor, compile, entry, hierarchy, fallback, dispatch, resolution, depth, validate, sealed, handler, cost