Advanced Class Methods in EK9

Some examples of the advanced class methods have already been show in other examples. This section will cover those ideas in more detail and highlight some of the compromises you will have to make to use them.

Really the advanced methods revolve around the dispatcher concept. This simple idea promotes the use of the Object Oriented approach - specifically polymorphism. It removes the need for casting and detection of types through instanceof. It also removes the need for switch on types.

Why no casting or instanceof

The converse question is - why does 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's the only thing the developer of that API wanted you to know. The details of what that type actually is should be hidden as that was the developers intent.

Adding casting and instanceof makes code brittle and goes against the general concept of information hiding and polymorphism. So when using API's it's best to avoid any attempt to get the actual type involved, where possible.

Designing your own API's

But there are times when you want to design an API's and use types in specific ways.

Double Dispatch

To avoid casting and instanceof a double dispatch approach can be taken.

Dispatcher keyword on methods

There are times when it is necessary to be able to process information where a more detailed knowledge of a type is required, as with the double dispatch approach. EK9 as a dispatcher keyword and concept just for this. There is an example used with Exceptions this includes a short discussion of how this is used. The key part in this example is the method 'private handleException() as dispatcher'.

A second example what also has a short discussion shows a similar approach but this time not using Exceptions. See 'build() as dispatcher' specifically in the example.

Relating two types

While the 'double dispatch' design pattern and the dispatcher keyword for types is really useful, the main power of the dispatcher keyword really comes into its own when there a need to relate two types together.

The following example is quite long and has two uses of dispatcher. The first is just a simple decorator type method, but the second is more interesting; it is aimed at demonstrating how it is possible to calculate the intersection between two shapes.

Given a number of Shapes; Ellipse, Circle, Square, Rectangle and Triangle - obtain the intersection between any two! Clearly to be able to calculate the intersection it is important to know the exact type of the two shapes involved.

This is where 'double dispatch' would come in handy, the EK9 language also supports dispatcher on methods with one or two parameters. But importantly it can also detect any ambiguities at compile time, it does this by looking at the class hierarchies and and traits used.

Example of Dispatcher

#!ek9
defines module introduction
 
  defines type
    List of Shape
     
  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: LinesIntersection("Line Intersection two squares (by function)")
        
    intersectSquareAndRectangle() as pure
      ->
        s1 as Square
        s2 as Rectangle
      <-
        intersection as 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
        
        assert 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
             
    //Used for different types of intersection
      
    Intersection as open
      message as 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)
      
      //... Other methods
    
    ArcIntersection is Intersection
      endPoint Coordinate: Coordinate() 
      centrePoint Coordinate: Coordinate()
      
      ArcIntersection()
        -> message as String
        super(message)
            
      //... Other methods
    
    LinesIntersection is Intersection
      endPoint Coordinate: Coordinate()
      end2Point Coordinate: Coordinate()
      
      LinesIntersection
        -> message as String
        super(message)      
      
      //... Other methods
          
    Shape as abstract
      stdout as Stdout: Stdout()
      
      draw() as abstract
      
      protected draw()
        -> message as String
        stdout.println("DRAW: " + message)
        
    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())
        
    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: ArcIntersection("Arc Intersection two circles")
      
      intersect() as pure
        ->
          s1 as Circle
          s2 as Ellipse
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection circle and ellipse")    
      
      intersect() as pure
        ->
          s1 as Ellipse
          s2 as Circle
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection ellipse and circle")
              
      intersect() as pure
        ->
          s1 as Circle
          s2 as Rectangle
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection circle and rectangle")

      intersect() as pure
        ->
          s1 as Rectangle
          s2 as Circle
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection rectangle and circle")
          
      intersect() as pure
        ->
          s1 as Circle
          s2 as Square
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection circle and square")
       
      intersect() as pure
        ->
          s1 as Square
          s2 as Circle
        <-
          intersection as Intersection: ArcIntersection("Arc Intersection squares and circle")
          
      intersect() as pure
        ->
          s1 as Rectangle
          s2 as Rectangle
        <-
          intersection as 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: LinesIntersection("Line Intersection ellipse and rectangle")
          
      intersect() as pure
        ->
          s1 as Circle
          s2 as Triangle
        <-
          intersection as Intersection: LinesIntersection("Line Intersection circle and triangle")
      
      intersect() as pure
        ->
          s1 as Triangle
          s2 as Triangle
        <-
          intersection as 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")      

      render()
        -> i as Intersection
        i.render()
        
  defines program
    testShapes()
      //Would really need actual details of what the circles and triangles were.
      //But this is just to show how 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)
          renderer.render(intersection)
//EOF

The output of the program above would be as follows:

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

While the example code and the output is long, it is aimed at highlighting the EK9 feature to be able to provide the developer with details of what actual classes are involved without the need for casting or instanceof. But is also highlights how methods can and should delegate process to functions where possible. This is because the function is really the smallest most isolated element to encapsulate functionality.

There are actually two example of dispatcher in the sample above; the first is just a simple single parameter dispatcher on the Renderer. In general these dispatcher methods do a little processing and the delegate through to the actual shape to trigger normal processing.

But the second example (on the Intersector) deals with the thorny issue of relating two classes by their actual type together. In general it is necessary deal with this N2 problem of relating two type together in a very methodical manner. This is what the EK9 language provides the framework for.

For Shapes this would involve trying to workout common denominators in intersections, then creating either a common set of methods or better functions that solve the intersection problem and reusing by altering the order of parameters passed in to each of the dispatcher methods.

Next Steps

Generics/Templates is covered in the next section, this is much more advanced.