How do I implement double dispatch in EK9?

← Design Patterns and Idioms · Ref: Q211

EK9 has built-in double dispatch via the 'as dispatcher' modifier. Mark a method as a dispatcher and provide overloads for specific types. The runtime selects the most specific match.

DISPATCHER METHOD

Mark the base method with 'as dispatcher':

  render() as dispatcher
    -> shape as Shape

This method is the dispatch entry point.

SPECIFIC OVERLOADS

Provide overloaded methods for specific subtypes:

  render()
    -> circle as Circle
    stdout.println("Rendering circle")
  render()
    -> rect as Rectangle
    stdout.println("Rendering rectangle")

RUNTIME DISPATCH

Calling render(someShape) dispatches to the most specific overload based on the actual runtime type of someShape.

NO VISITOR BOILERPLATE

Unlike the Visitor pattern, no accept() methods needed on element classes. The dispatcher handles type resolution automatically.

See Q60 for function dispatching. See Q103 for abstract classes. See Q105 for method dispatch. See Q254 for the Any type as dispatcher fallback. See Q255 for method resolution costs in dispatching.

Example

defines module qa.patterns.doubledispatch

  defines class

    Shape as abstract
      shapeName as String?

      default private Shape() as pure

      Shape() as pure
        -> n as String
        shapeName :=? String(n)

      name() as pure
        <- rtn as String: String(shapeName)

      override operator ? as pure
        <- rtn as Boolean: shapeName?

    Circle is Shape

      Circle() as pure
        super("Circle")

    Rectangle is Shape

      Rectangle() as pure
        super("Rectangle")

    // Renderer with dispatcher-based double dispatch
    Renderer
      stdout as Stdout: Stdout()

      // Base dispatcher method
      render() as dispatcher
        -> shape as Shape
        stdout.println("Rendering generic shape: " + shape.name())

      // Specific overload for Circle
      render()
        -> circle as Circle
        stdout.println("Rendering circle: " + circle.name())

      // Specific overload for Rectangle
      render()
        -> rect as Rectangle
        stdout.println("Rendering rectangle: " + rect.name())

      default operator ?

  defines program

    DoubleDispatchDemo()

      renderer <- Renderer()

      // === DIRECT CALLS ===

      renderer.render(Circle())
      renderer.render(Rectangle())

      // === DISPATCH VIA BASE TYPE ===
      // When called with Shape reference,
      // dispatcher resolves to most specific overload

      shapes <- List() of Shape
      shapes += Circle()
      shapes += Rectangle()

      for shape in shapes
        renderer.render(shape)

Common mistakes

E50060 — Shape does not have a label() method. The correct method name is name(). Calling a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.

Incorrect:

stdout.println("Rendering generic shape: " + shape.label())

Correct:

stdout.println("Rendering generic shape: " + shape.name())
Other ways to ask this
  • How do I use dispatcher for the visitor pattern in EK9?
  • How do I replace the visitor pattern in EK9?
  • How does multi-method dispatch work in EK9?

Coming from another language?

Java: Visitor pattern with accept/visit methods, or instanceof chains. Kotlin: when + is checks with sealed classes. C++: double dispatch via RTTI or visitor. Rust: enum match (no double dispatch). Go: type switch. EK9: 'as dispatcher' modifier with overloaded methods, runtime selects most specific match.

Keywords: pattern, idiom, design, multiple, dispatch, double, visitor, handler, overload, sealed, dispatcher, runtime, type