Advanced Class Methods in EK9
Some examples of the advanced class methods have already been shown elsewhere. This section will cover those ideas in more detail and highlight 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 any 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 has 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 traits used.
Example of Dispatcher
#!ek9 defines module introduction 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 dipatcher 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 are both 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 it also highlights how methods can and should delegate 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 work out 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.