Dynamic Classes in EK9
A dynamic class serves a similar purpose to a dynamic function. Unlike a dynamic function, it can be 'named', and a named dynamic class is fully usable as a class, as if you had declared it in the class block.
If you know Java, these look like anonymous classes. They can be used that way, and when named they can be used differently.
Defining a class deep in a function or method body may seem strange at first; the examples below show why it is useful. Note that the name of a dynamic class is 'pulled up' to module scope: unlike a Java inner class, it is visible across the module.
A dynamic class cannot extend a class, but it can have traits. To reuse what a 'base class' would have given you:
- Extract a trait from the 'base class' with the signatures you need.
- Give the dynamic class that trait, and delegate to an object that implements it.
- Provide or override the remaining methods in the dynamic class.
Why EK9 does it this way. The limit is deliberate: it promotes composition over inheritance. It may feel like a shortcoming. Go with the flow of the language to get the most from it; if you find that 'too painful', EK9 is probably not the language for you.
Quality Enforcement: the compiler applies its quality limits to dynamic class methods: cyclomatic complexity at most 45, cognitive complexity 35, nesting depth 6, and 100 statements per method (50 for an operator). Variable names, captured variables and fields included, must be descriptive. Dynamic classes have the same cohesion (LCOM4 at most 8) and coupling (Ce at most 12) limits as other classes. A capture that is never used is an error (E11018). Named dynamic classes are visible at module scope. See Code Quality for the full list.
Examples
- In the function setupSignalHandling, 'terminationHandler' and 'debugHandler' each define a dynamic class that captures variables and overrides methods. These classes are not named: they are held only through the object references 'terminationHandler' and 'debugHandler', so the class definitions are not reusable. Naming them would make them reusable.
- The TCP Server example has two unnamed dynamic classes with the trait TCPHandler. Both override the method connected to process the incoming traffic.
- The Mutex example shows the full syntax for creating a dynamic class.
The examples below show a named, reusable class.
Tuple like example
This is EK9's version of a Tuple: a dynamic class with no traits. The data captured into the Tuple becomes private, and is exposed only through the public methods defined on the class.
Points to note:
- 'ATuple' is used in functions defined both before and after it is declared.
- 'ATuple' is declared inside a for loop, and is still defined only once.
- Function 'getATuple' declares that it returns the 'ATuple' type and creates a new 'ATuple' with suitable data. The constructor must still be declared in 'ATuple'.
This defines classes in a dynamic but type safe way. If you are not from a dynamic language background, the syntax and arrangement may seem strange at first. It is an alternative way to define a class or tuple, and gives the code a more 'interpreted/dynamic' feel.
#!ek9 defines module site.dynamicClasses.tuple defines function printer2() -> personInfo as ATuple Stdout().println(`${ personInfo } but I can access ${personInfo.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() //Let's 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: name operator $ as pure <- rtn as String: `${message} ${name} ${dob} ${language}` operator #^ as pure <- rtn as String: $this default operator ? //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() -> tuple as ATuple stdout <- Stdout() stdout.println(`[ ${tuple} ] but I can access [ ${tuple.name()} ] directly if I wish`)
Download dynamicClasses_tuple.ek9
Running it prints:
Dynamic: John 1994-01-01 English Dynamic: Steve-1 1970-02-01 English Dynamic: Steve-2 1970-03-01 English Dynamic: Steve-3 1970-04-01 English Dynamic: Steve-4 1970-05-01 English Dynamic: Steve-5 1970-06-01 English Dynamic: Steve-6 1970-07-01 English Dynamic: Steve-7 1970-08-01 English Dynamic: Steve-8 1970-09-01 English Dynamic: Steve-9 1970-10-01 English Dynamic: Steve-10 1970-11-01 English [ Dynamic: John 1994-01-01 English ] but I can access [ John ] directly if I wish Dynamic: John 1994-01-01 English but I can access John directly if I wish Dynamic: John 1994-01-01 English but I can access John directly if I wish
You could define all your classes dynamically like this. From C++, Java or C# this will seem strange: there, a class is a big deal and may even need its own file.
The captured data is hidden, and users of the dynamic class can only call the public methods it defines. Those methods are type checked and resolved as you would expect, so renaming a method of a dynamic class is a safe refactoring.
It feels like a dynamically typed language, but it is statically typed and checked at compile time.
To capture fixed values into the 'tuple', give each captured value a name. Those names become the properties of the dynamic class.
Composition Dynamic Class Example
Composition shows how classes and traits are composed. With dynamic classes it goes further, and this example uses dynamic functions and dynamic classes together.
#!ek9 defines module site.dynamicClasses.composition defines trait LenValidator validateLength() as abstract -> p as String <- r as Boolean? CharValidator validateCharacters() as abstract -> p as String <- r as Boolean? Validator validate() as abstract -> p as String <- r as Boolean? defines class NoDollarValidator with trait of CharValidator override validateCharacters() -> p as String <- r as Boolean: p? and p not 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" require gt(val1) and lt(val1) val2 <- "Short" require not gt(val2) and lt(val2) val3 <- "1234567890123456789012345678901234567890Long" require gt(val3) and not lt(val3) lenValidator <- CheckLength(gt, max) with 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 default operator ? require lenValidator.validateLength(val1) require not lenValidator.validateLength(val2) require not lenValidator.validateLength(val3) checkContent <- CheckCharacter(prohibited) with trait of CharValidator override validateCharacters() -> p as String <- r as Boolean: true if p? r: p not contains prohibited default operator ? val4 <- "1234567890@not allowed" require checkContent.validateCharacters(val1) require checkContent.validateCharacters(val2) require checkContent.validateCharacters(val3) require not checkContent.validateCharacters(val4) validator1 <- (lenValidator, checkContent) with trait of LenValidator by lenValidator, CharValidator by checkContent default operator ? require validator1.validateCharacters(val1) and validator1.validateLength(val1) require validator1.validateCharacters(val2) and not validator1.validateLength(val2) require validator1.validateCharacters(val3) and not validator1.validateLength(val3) require not validator1.validateCharacters(val4) and validator1.validateLength(val4) validator2 <- (validator1, checkContent) with trait of Validator, LenValidator by validator1, CharValidator by checkContent override validate() -> p as String <- r as Boolean: validateLength(p) and validateCharacters(p) default operator ? require validator2.validate(val1) require not validator2.validate(val2) require not validator2.validate(val3) require not validator2.validate(val4) val5 <- "steve" validator3 <- (lenValidator) with 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) default operator ? require validator3.validate(val1) require not validator3.validate(val2) require not validator3.validate(val3) require validator3.validate(val4) require validator3.validate(val5) val6 <- "steve#host" validator4 <- (lenValidator) with trait of Validator, LenValidator by lenValidator, CharValidator notAllowed as String: "#" override validate() -> p as String <- r as Boolean: validateLength(p) and validateCharacters(p) override validateCharacters() -> p as String <- r as Boolean: false if p? r := p not contains notAllowed default operator ? require validator4.validate(val1) require not validator4.validate(val2) require not validator4.validate(val3) require validator4.validate(val4) require not validator4.validate(val5) require not validator4.validate(val6) Stdout().println("Every validator accepted and rejected as expected")
Download dynamicClasses_composition.ek9
Each require checks one answer; had any failed, the program would stop there. It prints:
Every validator accepted and rejected as expected
The 'Example' program first defines two dynamic functions, 'gt' and 'lt', both extending the abstract function 'discriminator'. They are tested, then captured and used in dynamic classes.
Dynamic Classes
- 'CheckLength' has the trait 'LenValidator' and overrides the method validateLength. It uses the captured function 'gt', and uses the 'max' variable as-is. The object is held in the variable 'lenValidator', which is used again later in composition.
- 'CheckCharacter' checks that the content does not contain a prohibited character. It has the trait 'CharValidator' and overrides the method validateCharacters.
Both are named dynamic classes, so you can use them elsewhere (with the captured variables they hold).
The Composition
- 'validator1' and 'validator2' are new dynamic classes with the traits 'LenValidator' and 'CharValidator'. The methods validateLength and validateCharacters are not reimplemented: they are delegated automatically to the variable objects each class captures. 'validator2' also has the trait 'Validator', and implements its method validate by combining the two delegated methods.
- 'validator3' also implements validate, but captures only 'lenValidator' for delegation. Its 'charValidator' is a new property of the dynamic class, of class 'NoDollarValidator'.
- 'validator4' delegates only the length check to a captured variable object, and implements validateCharacters itself, using a property defined in the dynamic class.
'Validator' is deliberately a trait of its own, rather than one that extends 'LenValidator' and 'CharValidator'. Had it extended them, 'LenValidator' would already be a trait of 'Validator', and 'validator2' could not list it again to delegate it: that is error E02090. Each trait is given by one route only.
Every dynamic class that captures variables declares default operator ?: the captured variables become its private properties, and it must say when it is set (E07236).
Summary
Dynamic classes can be short and flexible, and can be reused directly, through higher order functions, or through composition.
Why EK9 does it this way. The 'Single Responsibility Principle' and composition together reduce the need for inheritance as the only way to change behaviour. You define smaller, well defined functions, traits and classes, each with a single responsibility, then compose them in different ways. With polymorphism in functions as well as classes and traits, EK9 promotes reuse and composition of small, well rounded functions and classes.
With syntax for both inheritance/delegation and dynamic class definition, a declaration as short as:
validator1 <- (lenValidator, checkContent) with trait of LenValidator by lenValidator, CharValidator by
checkContent
default operator ?
creates a new class composed from a mix of functions and other classes that implement
traits.
Next Steps
The next section, Stream Pipelines, is more functional again: it uses collections and functions together for a range of processing. To stay with simple class-like aggregates, see Text Properties, which collect all the output text of a program or toolkit in one place.