Dynamic Classes in EK9

In a similar manner to the dynamic functions; the dynamic class can serve a similar purpose. But unlike dynamic functions; dynamic classes can be 'named' and can be 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; the example below shows it is very useful. It is important to note that the name of the dynamic class is 'pulled up' to module scope level. So unlike a 'Java inner class' it becomes visible in the module.

Dynamic Classes cannot extend an existing class but can use traits. While this might feel like a shortcoming (it has been limited deliberately). This is to promote composition rather than inheritance. Extract a Trait signature from your 'base class' that your class would have extended. Now use traits and delegation to utilise that functionality, also provide/override 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 be 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 an object reference ('terminationHandler' and 'debugHandler').The dynamic class definitions themselves are not reusable; they could have been; if they were 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 linked to above, 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 are; 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. If you are not from a dynamic language background; you may find this syntax and arrangement quite strange at first. It does provide for an alternative approach to class/tuple definition. This gives the code a more 'interpreted/dynamic' feel.

#!ek9
defines module introduction

  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

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

      //Lets have an inferred List of 'ATuple' and put the first entry in.
      tuples <- [tuple1]

      //This is both a declaration of a new type 'ATuple' and the creation of an object of type 'ATuple'
      //AND also indented is the implementation of 'ATuple'.
      //Plus we can put this in a loop if we need to 'ATuple' is only defined once!
      for i in 1 ... 10

        //Make the data up from base information and dynamic content.
        firstName <- name + "-" + $i
        dob <- dateOfBirth + Duration(`P${i}M`)

        //define, populate and get an object of that type
        tuple <- ATuple(name: firstName, dob: dob, language: "English") as class

          //We can also create new properties in the class directly if necessary
          //Rather than just capture.
          message as String: "Dynamic: "

          //You can even define a range of different constructors if you wish.
          //These are not automatically created, developer needs to define the 'interface'
          //That way if the names of variables captured are refactored/renamed, the rest of the code
          //using ATuple can stay the same.
          ATuple()
            ->
              name as String
              dob as Date
              language as String

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

          //Add in methods, hiding internal structure of object or meeting a trait type interface.
          name()
            <- rtn as String: firstName

          operator $ as pure
            <- rtn as String: `${message} ${firstName} ${dob} ${language}`
          operator #^ as pure
            <- rtn as String: $this

        //OK now just add that new tuple into the List of ATuple
        tuples += tuple

      //Display all those tuples.
      cat tuples > stdout

      //just to demonstrate calling a function that has 'ATuple' as a parameter.
      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 statically typed and checked at compile time. The creation of classes with EK9 is quite fluid and flexible.

If you want to use fixed values when capturing and defining the 'tuple', then the names of the parameters being captured but be given. Those names are then the same names that are used as properties in the dynamic class.

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) trait 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) trait 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)

      validator1 <- (lenValidator, checkContent) trait of LenValidator by lenValidator, CharValidator by checkContent

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

      validator2 <- (validator1, checkContent) trait of Validator, LenValidator by validator1, CharValidator by checkContent

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

      val5 <- "steve"

      validator3 <- (lenValidator) trait 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) trait 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/flexible in EK9. It reduces the need to use Inheritance as the only mechanism of behavioural change. It means smaller more well defined functions, traits and classes can be defined (with a single responsibility) 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 both inheritance/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.