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:
- The Exceptions example, with a short discussion: see 'private handleException() as dispatcher'.
- The traits example, not using Exceptions, with a short discussion: see 'build() as dispatcher'.
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()
Download advancedClassMethods_intersector.ek9
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.
- The first dispatcher, on the Renderer, takes one parameter. Like most single-parameter dispatcher methods, it does a little processing and then delegates to the actual shape.
- The second, on the Intersector, relates two classes by their actual types. This N2 problem has to be handled methodically, and EK9 provides the framework for it. For Shapes that means finding the common denominators in intersections, writing a common set of methods (or better, functions) that solve them, and reusing those by changing the order of the parameters each dispatcher method passes in.
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
Download advancedClassMethods_inherited_dispatcher.ek9
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.