Advanced Class Methods in EK9

Other pages have shown some advanced class methods. This section covers them in more detail, and the compromises you make to use them.

They revolve around the dispatcher. It promotes the object-oriented approach, specifically polymorphism, and removes the need for casting, for detecting types with instanceof, and for any switch on types.

Why no casting or instanceof

Why EK9 does it this way. Turn the question round: why would a developer ever need to 'know' what type they are dealing with? If an API returns a type, be it a class, record, trait or even a dynamic function, that is all its developer wanted you to know; what the type actually is was hidden on purpose. Casting and instanceof make code brittle and go against information hiding and polymorphism. So when you use an API, don't try to get at the actual type.

Designing your own API's

Sometimes, though, you design an API that uses types in specific ways.

Double Dispatch

To avoid casting and instanceof you can take a double dispatch approach.

Dispatcher keyword on methods

When processing needs more detailed knowledge of a type, as with 'double dispatch', EK9 has the dispatcher keyword. Two examples use it:

Relating two types

Double dispatch over one type is useful, but the dispatcher really comes into its own when you need to relate two types.

Take the shapes Ellipse, Circle, Square, Rectangle and Triangle, and find the intersection of any two. To calculate it you need the exact type of both shapes. A dispatcher method can take one or two parameters (E06320 for any other number), and the compiler detects any ambiguity from the class hierarchies and traits used (E05230).

The example below is long and uses dispatcher twice: a simple decorator-style method, and an intersection of two shapes.

Example of Dispatcher

#!ek9
defines module site.advancedClassMethods.intersector

  defines trait

    T1 allow only Square
      specialMessage() as pure
        <- rtn as String: "T1"
    T2
      simpleMessage() as pure
        <- rtn as String: "T2"

  defines function

    intersectSquares() as pure
      ->
        s1 as Square
        s2 as Square
      <-
        intersection as Intersection?
      require s1? and s2?
      intersection: LinesIntersection("Line Intersection two squares (by function)")

    intersectSquareAndRectangle() as pure
      ->
        s1 as Square
        s2 as Rectangle
      <-
        intersection as Intersection?
      require s1? and s2?
      intersection: LinesIntersection("Line Intersection of a Square and Rectangle (by function)")

  defines class

    Coordinate
      x <- Float()
      y <- Float()

      //Constructor
      Coordinate() as pure
        this(0.0, 0.0)

      //Constructor
      Coordinate() as pure
        ->
          initialX as Float
          initialY as Float

        require initialX? and initialY?
        x:=? Float(initialX)
        y:=? Float(initialY)

      x() as pure
        <- rtn as Float: Float(x)
      y() as pure
        <- rtn as Float: Float(y)

      operator $ as pure
        <- rtn as String: `${$x}, ${$y}`
      operator #^ as pure
        <- rtn as String: $this

      default operator ?

    //Used for different types of intersection

    Intersection as open
      message <- String()
      stdout as Stdout: Stdout()
      startPoint Coordinate: Coordinate(0.0, 0.0)

      Intersection() as pure
        -> message as String
        this.message:=? String(message)

      render()
        stdout.println(message)

      default operator ?

      //... Other methods

    ArcIntersection is Intersection
      endPoint Coordinate: Coordinate()
      centrePoint Coordinate: Coordinate()

      ArcIntersection() as pure
        -> message as String
        super(message)

      default operator ?

      //... Other methods

    LinesIntersection is Intersection
      endPoint Coordinate: Coordinate()
      secondLineEnd Coordinate: Coordinate()

      LinesIntersection() as pure
        -> message as String
        super(message)

      default operator ?

      //... Other methods

    Shape as abstract
      stdout as Stdout: Stdout()

      draw() as abstract

      protected draw()
        -> message as String
        stdout.println("DRAW: " + message)

      default operator ?

    Ellipse extends Shape as open
      override draw()
        draw("Ellipse")

    Circle is Ellipse
      override draw()
        draw("Circle")

    Rectangle is Shape
      override draw()
        draw("Rectangle")

    Triangle is Shape
      override draw()
        draw("Triangle")

    Square is Shape with trait of T1, T2
      override draw()
        draw(`Square ${T1.specialMessage()} ${T2.simpleMessage()}`)

      default operator ?

    BaseIntersector as abstract
      intersect() as pure abstract
        ->
          s1 as Shape
          s2 as Shape
        <-
          intersection as Intersection?

      intersect() as pure
        ->
          s1 as Circle
          s2 as Circle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection two circles")

      intersect() as pure
        ->
          s1 as Circle
          s2 as Ellipse
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection circle and ellipse")

      intersect() as pure
        ->
          s1 as Ellipse
          s2 as Circle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection ellipse and circle")
      intersect() as pure
        ->
          s1 as Circle
          s2 as Rectangle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection circle and rectangle")

      intersect() as pure
        ->
          s1 as Rectangle
          s2 as Circle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection rectangle and circle")
      intersect() as pure
        ->
          s1 as Circle
          s2 as Square
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection circle and square")
      intersect() as pure
        ->
          s1 as Square
          s2 as Circle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: ArcIntersection("Arc Intersection squares and circle")
      intersect() as pure
        ->
          s1 as Rectangle
          s2 as Rectangle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: LinesIntersection("Line Intersection two rectangles")
    SpecialIntersector extends BaseIntersector as abstract
      //Adding another method with additional types of shapes
      //But still this is an abstract class

      intersect() as pure
        ->
          s1 as Ellipse
          s2 as Triangle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: LinesIntersection("Line Intersection ellipse and rectangle")

      intersect() as pure
        ->
          s1 as Circle
          s2 as Triangle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: LinesIntersection("Line Intersection circle and triangle")

      intersect() as pure
        ->
          s1 as Triangle
          s2 as Triangle
        <-
          intersection as Intersection?
        require s1? and s2?
        intersection: LinesIntersection("Line Intersection two triangles")

      //Methods that just delegate to functions for the calculations.
      intersect() as pure
        ->
          s1 as Square
          s2 as Square
        <-
          intersection as Intersection: intersectSquares(s1, s2)

      intersect() as pure
        ->
          s1 as Square
          s2 as Rectangle
        <-
          intersection as Intersection: intersectSquareAndRectangle(s1, s2)

      intersect() as pure
        ->
          s1 as Rectangle
          s2 as Square
        <-
          intersection as Intersection: intersectSquareAndRectangle(s2, s1)

    //Finally make a concrete one - override the base intersect method that was abstract and mark it as a dispatcher
    Intersector extends SpecialIntersector
      override intersect() as pure dispatcher
        ->
          s1 as Shape
          s2 as Shape
        <-
          intersection as Intersection: Intersection("Intersection just two shapes!")

    Renderer

      //entry point for rendering via defining the dispatcher - this will find all sub type methods can call render
      render() as dispatcher
        -> s as Shape
        s.draw()

      //Just use default method for Circle and Ellipse, but others does something slight different

      render()
        -> s as Triangle
        s.draw()
        Stdout().println("Did draw triangle")

      render()
        -> s as Rectangle
        Stdout().println("Will Draw Rectangle")
        s.draw()

      render()
        -> s as T1
        Stdout().println("Drawing: " + s.specialMessage())

      render()
        -> s as T2
        Stdout().println("Drawing: " + s.simpleMessage())

      //With Square also implementing T1 and T2 you have to add this in. Which is correct else ambiguous.
      render()
        -> s as Square
        Stdout().println("Before Square")
        s.draw()
        Stdout().println("After Square")

  defines program
    testShapes()
      //Would really need actual details of what the circles and triangles were.
      //But this is just to show how a dispatcher would work.
      firstShapes <- [ Circle(), Ellipse(), Rectangle(), Square(), Triangle() ]
      secondShapes <- [ Circle(), Ellipse(), Rectangle(), Square(), Triangle() ]

      intersector <- Intersector()
      renderer <- Renderer()

      //Simple imperative loop this time rather than stream pipeline.
      for s1 in firstShapes
        for s2 in secondShapes
          renderer.render(s1)
          renderer.render(s2)
          intersection <- intersector.intersect(s1, s2)
          intersection.render()

The program prints:

DRAW: Circle
DRAW: Circle
Arc Intersection two circles
DRAW: Circle
DRAW: Ellipse
Arc Intersection circle and ellipse
DRAW: Circle
Will Draw Rectangle
DRAW: Rectangle
Arc Intersection circle and rectangle
DRAW: Circle
Before Square
DRAW: Square T1 T2
After Square
Arc Intersection circle and square
DRAW: Circle
DRAW: Triangle
Did draw triangle
Line Intersection circle and triangle
DRAW: Ellipse
DRAW: Circle
Arc Intersection ellipse and circle
DRAW: Ellipse
DRAW: Ellipse
Intersection just two shapes!
DRAW: Ellipse
Will Draw Rectangle
DRAW: Rectangle
Intersection just two shapes!
DRAW: Ellipse
Before Square
DRAW: Square T1 T2
After Square
Intersection just two shapes!
DRAW: Ellipse
DRAW: Triangle
Did draw triangle
Line Intersection ellipse and rectangle
Will Draw Rectangle
DRAW: Rectangle
DRAW: Circle
Arc Intersection rectangle and circle
Will Draw Rectangle
DRAW: Rectangle
DRAW: Ellipse
Intersection just two shapes!
Will Draw Rectangle
DRAW: Rectangle
Will Draw Rectangle
DRAW: Rectangle
Line Intersection two rectangles
Will Draw Rectangle
DRAW: Rectangle
Before Square
DRAW: Square T1 T2
After Square
Line Intersection of a Square and Rectangle (by function)
Will Draw Rectangle
DRAW: Rectangle
DRAW: Triangle
Did draw triangle
Intersection just two shapes!
Before Square
DRAW: Square T1 T2
After Square
DRAW: Circle
Arc Intersection squares and circle
Before Square
DRAW: Square T1 T2
After Square
DRAW: Ellipse
Intersection just two shapes!
Before Square
DRAW: Square T1 T2
After Square
Will Draw Rectangle
DRAW: Rectangle
Line Intersection of a Square and Rectangle (by function)
Before Square
DRAW: Square T1 T2
After Square
Before Square
DRAW: Square T1 T2
After Square
Line Intersection two squares (by function)
Before Square
DRAW: Square T1 T2
After Square
DRAW: Triangle
Did draw triangle
Intersection just two shapes!
DRAW: Triangle
Did draw triangle
DRAW: Circle
Intersection just two shapes!
DRAW: Triangle
Did draw triangle
DRAW: Ellipse
Intersection just two shapes!
DRAW: Triangle
Did draw triangle
Will Draw Rectangle
DRAW: Rectangle
Intersection just two shapes!
DRAW: Triangle
Did draw triangle
Before Square
DRAW: Square T1 T2
After Square
Intersection just two shapes!
DRAW: Triangle
Did draw triangle
DRAW: Triangle
Did draw triangle
Line Intersection two triangles

Summary

The example and its output are long, to show how EK9 tells you the actual classes involved without casting or instanceof. It also shows methods delegating to functions where possible: the function is the smallest, most isolated unit of functionality.

Adding handlers in a sub-type

A dispatcher in an open class can be extended: a sub-type adds the handlers for the types it knows about. It does not re-declare the entry point, and the new handlers are not marked override: they take other parameter types than the entry point, so they are new methods (an override has exactly the parent's parameter types). EK9 gives the sub-type an entry point that dispatches over the handlers of the whole chain and falls back to the super's, so a call through a reference of the base type still reaches the sub-type's handler.

#!ek9
defines module site.advancedClassMethods.inheritedDispatcher

  defines class

    Shape as open
      shapeName() as pure
        <- rtn as String: "shape"

    Circle extends Shape
      override shapeName() as pure
        <- rtn as String: "circle"

    Triangle extends Shape
      override shapeName() as pure
        <- rtn as String: "triangle"

    Base as open
      render() as dispatcher
        -> shape as Shape
        <- rtn as String: `fallback ${shape.shapeName()}`

      render()
        -> circle as Circle
        <- rtn as String: `base ${circle.shapeName()}`

    Sub extends Base
      render()                              //a new handler - no 'override'
        -> triangle as Triangle
        <- rtn as String: `sub ${triangle.shapeName()}`

  defines program

    Rendering()
      stdout <- Stdout()
      renderer as Base: Sub()
      stdout.println(renderer.render(Circle()))     //base circle
      stdout.println(renderer.render(Triangle()))   //sub triangle
      stdout.println(renderer.render(Shape()))      //fallback shape

It prints:

base circle
sub triangle
fallback shape

Every dispatcher check runs on the sub-type too: a handler for a type outside the entry point's hierarchy (E05210), or two handlers that a type matches equally well (E05230), is an error there. To change the fallback itself, the sub-type can still override the entry point ('override render() as dispatcher'). The built-in Aspect uses this: its afterAdvice is a dispatcher, so an aspect that returns its own PreparedMetaData from beforeAdvice adds an afterAdvice handler for it.

Next Steps

The next section, Generics/Templates, is more advanced still.