Dynamic Classes in EK9

In a similar manner to the dynamic functions, the dynamic class can serve a similar purpose in some ways. But unlike dynamic functions, dynamic classes can be 'named' and are elevated to be fully usable as a class as if it were declared in the class block (when named).

If you are familiar with Java you may initially think these are just like anonymous classes; they can be, but they can also be used in a different way (when named).

Initially it may seem a little strange that it is possible to dynamically define a class deep in a function or method body; and that class can then be used as if it were defined at module level (when named); but as the example below shows it is very useful.

Dynamic Classes cannot extend an existing class but can use traits. While this might feel like a shortcoming (it has been limited deliberately) - remember you can and should use composition where possible! Extract a Trait signature from your 'base class' that your class would have extended. Now use traits and delegation to utilise that functionality, also providing/overriding the methods in your dynamic class. Remember try and go with the flow of this to make the most of the language, if you find it 'too painful' it would probably best to use an alternative to EK9.

Examples

The function setupSignalHandling and specifically 'terminationHandler' and 'debugHandler' both define dynamic classes, they capture variables and override methods. But take note that the dynamic classes they create are not named. They are held via object reference ('terminationHandler' and 'debugHandler'), so the dynamic class definitions themselves are not reusable; they could have been if named. The TCP Server example shows two dynamic classes with a trait TCPHandler. Both of these classes override the method connected to be able to process the incoming traffic. Again these dynamic classes are un-named. The example later shows a named and reusable class.

The Mutex example also shows how a dynamic class can be employed. It shows the full syntax when creating the dynamic class.

Tuple like example

All of the examples so far have demonstrated a fairly simple use of dynamic classes, this next inline example demonstrates the EK9 version of a Tuple. In fact EK9 just uses a dynamic class for this but without any traits. The data captured into the Tuple becomes private and only exposed via public methods defined on the class

The interesting points to note in the example, is the use of 'ATuple' in functions defined both before and after 'ATuple' is declared, and that 'ATuple' is declared inside a for loop. The other main point is that function 'getATuple' can just declare it returned the 'ATuple' type and then can also create a new instance of 'ATuple' with suitable data (Constructor must still be declared in 'ATuple').

This provides a way to define classes in a very dynamic, but type safe manner. You may find this syntax and arrangement quite strange at first if you are not from a dynamic language background. It does provide for an alternative approach to class/tuple definition. In general it gives the code a more 'interpreted/dynamic' feel.

#!ek9
defines module introduction

  defines type
    List of ATuple

  defines function
    printer2()
      -> data as ATuple
      Stdout().println("[" + data + "] but I can access [" + data.name() +"] directly if I wish")

    //Note how we can just use 'ATuple' in a normal way by creating an instance of it.
    getATuple()
      <- rtn as ATuple: ATuple("John", 1994-01-01, "English")

  defines program
    ShowTupleClass()

      stdout <- Stdout()

      name <- "Steve"
      dateOfBirth <- 1970-01-01
      language <- "English"

      //Just access a function that returns 'ATuple' - even though 'ATuple' is not defined yet!
      tuple1 <- getATuple()

      tuples <- [tuple1]

      for i in 1 ... 10

        firstName <- name + "-" + $i
        dob <- dateOfBirth + Duration("P"+$i+"M")

        //Here is where the tuple is actually defined.
        tuple <- ATuple(firstName, dob, language) as class
          message as String: "Dynamic: "

          ATuple()
            ->
              name as String
              dob as Date
              language as String

            this.firstName = name
            this.dob = dob
            this.language = language

          name()
            <- rtn as String: firstName

          operator $
            <- rtn as String: message + firstName + " " + $dob + " " + language
          operator #^
            <- rtn as String: $this

        tuples += tuple

      cat tuples > stdout

      printer1(tuple1)
      printer2(tuple1)
      printer2(getATuple())

  defines function
    printer1()
      -> data as ATuple
      stdout <- Stdout()
      stdout.println("[" + data + "] but I can access [" + data.name() +"] directly if I wish")
//EOF

The example above demonstrates that it is possible to define all your classes dynamically like this if you wanted to. Coming from a C++, Java or C# background this will probably seem strange, normally classes are a big deal and even need to be in their own files.

The nice thing about the approach above, is that data captured is hidden and users of the dynamic class can only use the public methods defined. The methods are all type checked and resolved as you would expect. So refactoring dynamic classes altering method names is safe.

This gives the impression of a more dynamic typed language, but is in fact strongly typed and checked at compile time. The creation of classes with EK9 is quite fluid and flexible.

There is one major restriction when creating dynamic classes and that is the variables that are to be captured but must be variables. They cannot be literals or function/method calls. This is because when the dynamic class is created with the captured data, each data has to be given a field name. The idea of using '_1' or something like that is possible but not acceptable. Meaningful names provide much more value in terms of readability. But at the cost of having to declare variables with names to capture.

Composition Dynamic Class Example

The section on composition shows how classes and traits can be used in composition. This power is magnified when used with dynamic classes as the following example demonstrates.

In fact this example shows how dynamic functions and dynamic classes can be used together.

#!ek9
defines module introduction

  defines trait
  
    LenValidator
      validateLength() abstract
        -> p as String
        <- r as Boolean      
    
    CharValidator
      validateCharacters() abstract
        -> p as String
        <- r as Boolean
        
    Validator with trait of LenValidator, CharValidator
      validate()
        -> p as String
        <- r as Boolean: validateLength(p) and validateCharacters(p)

  defines class
  
    NoDollarValidator trait of CharValidator
      override validateCharacters()
        -> p as String
        <- r as Boolean: p? and not p.contains("$")

  defines function
  
    discriminator as abstract
      -> s as String
      <- rtn as Boolean
  
  defines program
  
    Example

      min <- 9
      max <- 40
      prohibited <- "@"
      
      gt <- (min) is discriminator as function
        rtn: length s > min

      lt <- (max) is discriminator
        rtn: length s < max

      val1 <- "Some Value"
      assert gt(val1) and lt(val1)
      val2 <- "Short"
      assert not gt(val2) and lt(val2)
      val3 <- "1234567890123456789012345678901234567890Long"
      assert gt(val3) and not lt(val3)

      lenValidator <- CheckLength(gt, max) of LenValidator as class
        override validateLength()
          -> p as String
          <- r as Boolean: false
          if p?
            r := gt(p) and length p < max //So I can just use gt and lt or mix and match        

      assert lenValidator.validateLength(val1)
      assert not lenValidator.validateLength(val2)
      assert not lenValidator.validateLength(val3)

      checkContent <- CheckCharacter(prohibited) of CharValidator
        override validateCharacters()
          -> p as String
          <- r as Boolean: true
          if p?
            r: ~p.contains(this.prohibited)

      val4 <- "1234567890@not allowed"

      assert checkContent.validateCharacters(val1)
      assert checkContent.validateCharacters(val2)
      assert checkContent.validateCharacters(val3)
      assert not checkContent.validateCharacters(val4)

      validate1 <- (lenValidator, checkContent) of LenValidator by lenValidator, CharValidator by checkContent

      assert validate1.validateCharacters(val1) and validate1.validateLength(val1)
      assert validate1.validateCharacters(val2) and not validate1.validateLength(val2)
      assert validate1.validateCharacters(val3) and not validate1.validateLength(val3)
      assert not validate1.validateCharacters(val4) and validate1.validateLength(val4)

      validate2 <- (validate1, checkContent) of Validator, LenValidator by validate1, CharValidator by checkContent

      assert validate2.validate(val1)
      assert not validate2.validate(val2)
      assert not validate2.validate(val3)
      assert not validate2.validate(val4)

      val5 <- "steve"
      
      validator3 <- (lenValidator) of Validator, LenValidator by lenValidator, CharValidator by charValidator
        charValidator as CharValidator: NoDollarValidator()
        override validate()
          -> p as String
          <- r as Boolean: true
          if p not matches /[S|s]te(?:ven?|phen)/
            r: validateLength(p) and validateCharacters(p)
      
      assert validator3.validate(val1)
      assert not validator3.validate(val2)
      assert not validator3.validate(val3)
      assert validator3.validate(val4)
      assert validator3.validate(val5)
            
      val6 <- "steve#host"
            
      validator4 <- (lenValidator) of Validator, LenValidator by lenValidator, CharValidator
        notAllowed String: "#"

        override validateCharacters()
          -> p as String
          <- r as Boolean: false
          if p?
            r:= p not contains notAllowed
      
      assert validator4.validate(val1)
      assert not validator4.validate(val2)
      assert not validator4.validate(val3)
      assert validator4.validate(val4)
      assert not validator4.validate(val5)
      assert not validator4.validate(val6)
//EOF

There is quite a bit going on in the above example, the first part of the 'Example' program defines two dynamic functions 'gt' and 'lt', these are both functions that extend abstract function 'discriminator'. These are tested and then captured and used in dynamic classes.

Dynamic Classes

The 'CheckLength' dynamic class has a trait of 'LenValidator' and overrides method validateLength. This method uses the captured function 'gt', but just does the 'max' variable as-is. This variable object is held as a class property called 'lenValidator'. This too is used later in composition.

A second dynamic class 'CheckCharacter' is used to check the content does not contain a specific prohibited character. This has the trait of 'CharValidator' and overrides method validateCharacters.

As both 'CheckLength' and 'CheckCharacter' are named dynamic classes they can be used in other contexts if required (which will include the captured variable they hold).

The Composition

The objects 'validate1' and 'validate2' define new dynamic classes that have traits of 'LenValidator' and 'CharValidator'. The important point to note here that the implementation of methods validateLength and validateCharacters are not reimplemented but are automatically delegated. Note that they are delegated to the variable objects they capture.

Object 'validate3' does reimplement method validate, but in this scenario only 'lenValidator' is captured for delegation the 'charValidator' is created as a new property on the dynamic class and uses class 'NoDollarValidator'.

Finally object 'validate4' only delegates the length validation to a captured variable objects and then implements method validateCharacters using a property defined in the dynamic class itself.

Summary

The examples above clearly demonstrate that dynamic classes can be very short, flexible and can also be re-used either directly, through high order functions or through composition.

The idea of the 'Single Responsibility Principle' and composition is very powerful and flexible in EK9. It reduces the need to use Inheritance as the only mechanism of extensibility. It means smaller more well defined functions, traits and classes can be defined and then composed in a range of different ways.

By adopting polymorphism in functions as well as classes and traits; EK9 strongly promotes re-use and composition of small well rounded functions/classes. With the syntax to support delegation and dynamic class definition; simple one liners like:
validate1 <- (lenValidator, checkContent) of LenValidator by lenValidator, CharValidator by checkContent
provide a really lightweight declarative mechanism of creating a new class composed of a mix of functions and other classes that implement traits.

Next Steps

The next section is again more functional and details Stream Pipelines this makes the most of collection and functions to accomplish a range of processing. If you want to stay with class type simple aggregates; take a look at Text Properties. These provide a very simple mechanisms to collect all the output text you might want to use in a program or toolkit in one logical place.