Traits in EK9

The trait has been touched on in the structure section. This shows how a trait can be used to control/limit class extension.

Main uses for traits

If you are familiar with Java you may consider that traits are just like a Java Interface; there are some similarities but also several features in EK9 where traits play a significantly different role. In fact the trait unlike the Java Interface provides real structural enabling/constraining power. In many ways it provides much more control than a class, but less implementation functionality; and no retained state.

Take your time when reading this section and reviewing the example. If you've used other programming languages don't assume that the trait in EK9 is just some minimal cut down abstract class that can be skimmed over.

The main features/characteristics

Traits Example

There are quite a few ideas covered in the following example; these are discussed following the example.

The program 'ProcessorTest' below creates a number of 'Processors' and then iterates over those 'Processors' and calls process to get a 'ProcessingResponse'. That response is then passed to 'ResultBuilder' via the build method to create an output String.

There are different 'Processor' classes that exhibit the trait of 'Processor'. This is to demonstrate how different implementations can exhibit the same trait (they all meet the same signature). The 'Processor' trait also inherits from traits 'CostAssessment' and 'Moniterable'. The Method lowCost is declared in both of those traits (to demonstrate a name clash).

The 'DelegatingProcessor' shows how composition works. There are also two classes of 'StringResponse' and 'StructuredResponse' that both exhibit the trait of 'ProcessingResponse'. This provides a method called 'result()'.

The Example

#!ek9
defines module introduction

  defines trait

    CostAssessment
      lowCost() as pure
        <- rtn <- true

    Moniterable
      available() as pure
        <- rtn <- true
      lowCost() as pure
        <- rtn <- false

    Processor with trait of Moniterable, CostAssessment
      process()
        <- response as ProcessingResponse?

      override lowCost() as pure
        <- rtn as Boolean: CostAssessment.lowCost()

    ProcessingResponse allow only StringResponse, StructuredResponse
      result()
        <- rtn as String?

  defines class

    StringResponse with trait of ProcessingResponse as open
      theResponse as String: String()

      StringResponse() as pure
        -> response as String
        theResponse :=: response

      StringResponse() as pure
        this(String())

      override result()
        <- rtn as String: theResponse

    StructuredResponse is StringResponse
      contentType as String: String()

      StructuredResponse() as pure
        ->
          response as String
          contentType as String
        super(response)
        this.contentType :=: contentType

      StructuredResponse() as pure
        ->
          response as String
        this(response, "text/plain")

      contentType() as pure
        <- rtn as String: contentType

    SimpleProcessor with trait of Processor

      override process()
        <- response as ProcessingResponse: StringResponse("Simple Message")

    DelegatingProcessor with trait of Processor by proc
      proc as Processor?

      DelegatingProcessor() as pure
        -> processorToUse as Processor
        proc: processorToUse

      DelegatingProcessor() as pure
        this(SimpleProcessor())

      override lowCost() as pure
        <- rtn as Boolean: false

     XMLProcessor with trait of Processor
      override process()
        <- response as ProcessingResponse: StructuredResponse("<tag>Simple Message</tag>", "text/xml")
      override lowCost() as pure
        <- rtn as Boolean: false

    //I've used a slightly different layout here and also used named parameters as an example.
    JSONProcessor with trait of Processor
      override process()
        <- response as ProcessingResponse: StructuredResponse(
          response: `{"name": "John", "age": 31, "city": "New York"}`,
          contentType: "application/json"
        )

    ResultBuilder

      build() as dispatcher
        -> response as ProcessingResponse
        <- rtn as String: response.result()

      build()
        -> response as StringResponse
        <- rtn as String: `"${response.result()}"`

      build()
        -> response as StructuredResponse
        <- rtn as String: `${response.contentType()} (( ${response.result()} ))`

  defines program

    ProcessorTest
      stdout <- Stdout()

      processors <- [ SimpleProcessor(), XMLProcessor(), DelegatingProcessor(JSONProcessor()), JSONProcessor()]

      for processor in processors
        response <- processor.process()
        stdout.println(ResultBuilder().build(response))

//EOF

The output of the program above would be:

"Simple Message"
text/xml (( <tag>Simple Message</tag> ))
application/json (( {"name": "John", "age": 31, "city": "New York"} ))
application/json (( {"name": "John", "age": 31, "city": "New York"} ))
        

The class 'ResultBuilder' is acting as a decorator (design pattern), it has the ability to access the additional method contentType() because the dispatcher has ensured the right method is called with the right type of parameter.

What has been shown in the Example

The example above is quite sophisticated in terms of structure control. Each aspect of traits is discussed below.

Constraining Extension

As outlined above; a trait can be used to control the degree to which classes may extend the trait. This can be very useful in the development of API's, library classes and sub systems. If the developer needs to ensure that the traits/classes they have developed can be used (but not extended) by users of their API; then applying constraints on a trait provides a flexible way of accomplishing this. EK9 also has the limitation of extending classes by default if they are not open.

In some other languages the use of final is the only mechanism to limit extension. Some languages have sealed types similar to this concept.

See:

ProcessingResponse allow only StringResponse, StructuredResponse

Providing a signature interface

This is the traditional use for interfaces/traits it really just consists of a set of method signatures. The trait can then be used in abstract way. Both 'Processor' and 'ProcessingResponse' are really just signatures, although 'ProcessingResponse' also constrains the classes that may use it as a trait.

With traits; concrete (non abstract) methods can be implemented (a little like default methods on interfaces in Java). The trait 'Processor' has such a method; lowCost demonstrates this. The class 'XMLProcessor' overrides this method to demonstrate that it is possible alter the implementation if needs be.

Trait inheritance

Inheritance/multiple inheritance of traits is also supported as shown with 'CostAssessment', 'Moniterable' and its use in 'Processor'. However, there is a significant point to note here, where two methods like lowCost conflict - the trait or class where they are used together has to override that method.

The resolution of this conflict is shown in the 'Processor' trait. It overrides the lowCost method and then delegates the call to CostAssessment.lowCost() it could have used Moniterable.lowCost() or indeed just used true/false.

Composition

One of the biggest benefits of defining traits in EK9 is the ability to use automatic composition. In the example there are a number of 'Processors' these are aimed to processing data to produce a 'ProcessingResponse'. The DelegatingProcessor with trait of Processor by proc demonstrates the use of composition.

The by proc syntax is the key part here; this syntax triggers the delegation of the trait methods to the 'proc' private field object. This simple directive means that the class can now direct all calls to that delegate, but can optionally elect to override and provide the method implementations itself. Just take a moment and think about what that means; delegation is now as easy as inheritance! It might not always be appropriate, but now you have a simple syntax option to adopt delegation.

As you can see it is not necessary to implement all the methods that 'Processor' defines when using composition. All the other 'Processors' have to implement the abstract methods defined in 'Processor':

The additional use of by proc is the mechanism for composition; this expects a Property/Field in the 'DelegatingProcessor' called 'proc' which is of type 'Processor'. Just by using this statement (and creating 'proc' and setting it) all the method calls to the 'DelegatingProcessor' will be automatically routed to that delegate.

The construction of the 'DelegateProcessor' with DelegatingProcessor(JSONProcessor()) could have been done with an 'XMLProcessor' parameter and the behaviour would have been altered. This is composition; it is way more flexible in many ways than inheritance. It also enforces/ reinforces encapsulation and direct re-use.

Importantly it is possible to use this with any number of traits; and so compose classes out of a range of other classes with compatible traits; through the use of composition. As classes can exhibit multiple traits; class behaviour can be composed from multiple classes, in a way similar to multiple class inheritance (but much simpler and more maintainable).

It is still possible to override the methods for some parts of the 'Processor' signature in the class should you wish to. So in the example above, the method lowCost is not delegated but is re-implemented. But method available has been delegated.

Thoughts

The use of traits provides way more than just a 'set of method signatures'. Yes is can and should be used for that. It has way more power; by facilitating easy delegation and constraining the classes that can use it. It is not just an abstract class!

Please take some time to understand the trait because it is quite different to what most other programming languages offer.

Dispatcher

While not really related directly to traits the class 'ResultBuilder' has a method called build. That method is marked up as a dispatcher. Interestingly that class has two other methods also called build, but with different parameter types.

The important point of the dispatcher syntax is that it removes the need for detecting the types of classes/traits. This is done with the build method and parameter of 'ProcessingResponse'. This is just the base trait, but by marking the build method as a dispatcher; EK9 will look at the actual type of Object passed in and then automatically call the appropriate build method. This is covered in more detail in section advanced class methods.

This feature removes the need to cast Objects or use any syntax like instanceof, EK9 does not support casting or the detection of types. You could view the class 'ResultBuilder' as a sort of 'switch statement for types'. It has a default behaviour, but will attempt to find a method that accepts the type the parameter actually is - rather than just the type that was passed in.

The biggest advantage of this approach is the retention of Object-Oriented approach and the avoidance of 'class cast exceptions'. Using 'casting' and 'instanceof' techniques creates brittle code that does not refactor easily.

Summary

Traits can be used in a very sophisticated and flexible manner to provide control and abstraction. They allow Objects to be viewed in different but complementary ways and facilitate design through composition rather than just inheritance.

If you ever find you need functionality from one class in another class, don't just think of an inheritance structure as this can be very limiting. Consider traits and composition by refactoring classes into smaller units and extracting method signatures out to traits.

You may find that holding what would have been 'class properties' in a record, then using traits to define control and 'signatures' with a range of classes and dynamic classes an alternative approach to just using inheritance. If you also combine this approach with 'function delegates', you have quite a different way to implement functionality.

It may take some time for patterns to emerge that make the most of the delegation facilities in EK9.

Next Steps

There is more on this subject in the section on composition.