Structure of EK9

An EK9 source code file can have any file name (but must have a file extension of .ek9); the first line must only contain #!ek9 this is so that the file can be processed by the compiler. The structure then follows the following form:

The Constructs

As you can see from the above list, there are quite a few constructs. It is not necessary to know them all before getting started. If you are a more senior/experienced developer you will probably want to go through all of these constructs at some point. If you've done some development before at the 'mid' level then just go as far as traits and come back to generics and the others later. For junior developers or those just starting out, really understand programs and functions; then just jump over to basics, operators and flow control. Come back to records and classes later, you can build up in phases. It would be quite easy to become overwhelmed with lots of new concepts and terminology, you need to pace yourself when learning something new.

Remember EK9 is designed to go from small scale applications right up to large scale solutions and as such has incorporated a lot of design patterns/framework ideas/API's into the language itself (it is big). EK9 is more like a 'tool box'; if you really don't need to use the pliers (components) leave them in the box, but when you do need them they are available.

You can just use programs and functions if that's all you need (like 'C'), only use what you need.

If you've not seen any EK9 code yet, then take a look here for the hello world example or jump to the program below.

In the end, if you plan to build a full stack application and include microservices you will end up using these same sort of API's and ideas even if you use third party tools/API's or frameworks. But if you just need a quick program to parse a JSON, XML or CSV file then program might be the only construct you need. You might want to structure the program a bit and break it down and use a few functions as well.

As you go through these various constructs below; the examples become more and more detailed and more assumptions around software development at an enterprise scale are made. If you are new to these ideas then there are links on this page that reference resources that explain the concepts (like Functional Programming, Object Oriented Programming, Aspect Oriented Programming, Inversion of Control and Dependency Injection).

You should be able to 'stop' at specific constructs and just use what you've learned so far. For inexperienced developers - take your time and build your knowledge slowly. For experience developers go for it. This should look like all the techniques you've been using before, but all built into the language.

EK9 is designed to enable developers to mix a range of these techniques to suit themselves and find a balance for their team. Each of the above techniques has power and capability, but the blend of techniques employed will depend on the type of software you are developing and the range of skills/experience in the team.

Getting started with EK9 file layout

Module
  • #!ek9
  • defines module introduction

Module - is really just a name space for constructs to be held in. The example above uses introduction but it could have been any name including a conceptual hierarchy such as com.system.introduction. If you intend to publish your code to the EK9 repository or even make it reusable internally as a versioned release then selecting a suitable module name is important. Take a look at the package directive to understand this in more detail.

It is important to note that any number of files can be used for source code and each of those files can use the same module name space; when this is done all constructs defined are visible to each other as they are in the same name space.

There is no linked visibility between name spaces where a conceptual hierarchy naming is employed, they are in effect fully separated. For example com.p1.submodule is fully separate from com.p1 even though their base names are the same there is no implied or secret access. The name space must have lower case letters. Each sequence of characters can then be punctuated with a '.'.

References

See the detailed section on references to understand how to access constructs from other name spaces. If other constructs from other name spaces are to be referenced, then the references must come after the module declaration and before the first construct declaration.

When referencing constructs in other modules, the reference must be fully qualified, there are no wildcards! Best practice is to create a source file in your module that references all the other components you wish to access. This may seem like a 'painful' activity if you have many linkages to other modules, it is designed to be 'painful'!

The point of creating this pain is to try to reduce the linkage between module constructs; to try and encourage encapsulation, information hiding, limit cyclomatic complexity and reduce coupling. If you find you have too many references then look to refactor and start to use components.

The Constructs

After the module declaration the developer can define a range of different constructs.

In general the structure of the file is marked by a defines directive this indicates what sort of declarations will follow, so unlike some languages that require the declaration of the sort of item being declared (such as a type, function, class or a trait). EK9 now just expects all declarations after this to be of the same sort of construct, until it encounters the next defines statement. This approach is similar to the 'Pascal' programming language.

EK9 allows definition and repetition of different sequences of constructs in any order. But this would be considered bad 'style'.

Constant

While it is not necessary to define constant constructs if none are needed; it is considered good 'style' to define all the constants in a block towards the top of a file. The main reason for this is convention, driven from other languages that developers have become used to.

Constants can only be basic built-in types. Even if you define your own classes you cannot use them as constants. See Components and Applications and Dependency Injection to be able to use your own classes in a manner similar to constants.

The concept of a constant in EK9 is really preserved for real world primitive ideas such as PI. In addition constants cannot be calculated. If there were two constants of PI and radius it would not be possible to define the third constant of area as PI*radius^2 as this involves an expression (you would create a function to do this). Note that all constants are public in nature (meaning they are openly accessible).

  • #!ek9
  • defines module introduction
  •   defines constant

Once the above constant construct declaration has been made; the developer can then make a number of statements to declare constants, such as:

  • PI 3.142
  • e 2.71828

The ← symbol which in EK9 is written with '<' and '-' in combination is used to define a new constant and assign it to a value (constants are immutable - i.e cannot be changed). This also uses type inference, so in the above example the constant PI has the type of Float with a value of 3.142. The same ← symbol can also be used with variables (shown later).

There is no need for let, def, const, final etc for constants. So in many ways this approach is similar to the Pascal Programming Language but without the need to declare what the type of the constant is.

Note the indentation below - two spaces (that's the recommendation, start your indentation arguments now), not tabs (configure your editor to expand tabs to spaces) - but consistently use the same number of spaces in an EK9 source file.

Definition of two constants

#!ek9
defines module introduction
  defines constant
  
    PI <- 3.142    
    e <- 2.71828
//EOF

Other than constants; types can also be defined. The convention is to define the type constructs after the constant constructs.

Type

Types have a wider use than just a simple enumeration(shown below); constraints, aliases and generic forward declarations are all declared as a type construct.

By using the statement below any number of types can be defined in succession, they do not have to be defined in separate source files (as must be done in Java for example).

  • defines type

A complete example is given below with the four valid values that the enumerated type can take.

#!ek9
defines module introduction
  defines type
  
    CardSuit
      Hearts
      Diamonds
      Clubs
      Spades
//EOF

While on the surface these enumerated types look very simple (almost primitive and a little like YAML), they enable strong typing much like Enumerations in other languages. In the example above a variable of type CardSuit can only be set to one of those values listed.

Enumerations also come with a range of built in operators that don't need coding up. For example the $ operator that converts the enumeration value to a String. Hearts above would become "Hearts" but now with a type as a String. But the enumerated types are intentionally very limited, you cannot add methods to them like you can in other languages. To do that you need to use classes.

Hopefully by now you will see that EK9 deliberately limits and constrains features and capabilities of specific constructs to encourage the use of the appropriate construct. Limiting constants to encourage components, limit enumerations to encourage classes, this is a common theme in EK9. If you get to the point where you feel a construct should have a certain capability but it doesn't; its probably time to look at an alternative construct.

Program
  • defines program

The program is the only way of starting any sort of application, you can define any number of programs after the above directive. If you had no need of constants or types you can just use the program construct and then define your program.

  • HelloWorld()

The above looks just like a method or a function and in a way it is just that. The example of 'hello world' was outlined in the introduction. Here it is again but this time there are two programs and a constant. This approach enables a developer to capture all the programs they wish to develop in a controlled manner. Clearly which program you decide to run is up to you when you call it from the command-line.

#!ek9
defines module introduction
  defines constant
    worldMessage <- "Hello, World"
    
  defines program
    HelloWorld()
      stdout <- Stdout()
      stdout.println(worldMessage)
      
    HelloMars()
      stdout <- Stdout()
      stdout.println("Hello, Mars")
//EOF

Lets assume this is stored in a file called helloworlds.ek9 we would run those programs with the following commands (note how it is necessary to specify the program you wish to run:

  • $ ek9 helloworlds.ek9 -r HelloWorld
  • $ ek9 helloworlds.ek9 -r HelloMars
  • On Linux/Unix/MacOS you can use this as well/instead
  • $ ./helloworlds.ek9 -r HelloWorld
  • $ ./helloworlds.ek9 -r HelloMars

There are more details in the programs section; this covers how to pass parameters from the command line into a program.

If you'd now like to just try out some programs and add in if/else/switch flow control just jump over to that section. You can just code something simple up just in a program if you want to. Then come back and break that functionality down to include functions if the code gets too long or you want to re-use parts of the code in other programs.

EK9 is designed to be able to do small bits of simple development with ease. There is no need to jump right into classes and generics; if you just need a program to load up a text file and process parts of that text to produce some type of output for example.

Record

This is the first construct that is an aggregate used in EK9 and is the simplest; tuples are available in EK9, but are called dynamic classes. Use these for data structures where the types are different in the aggregate or use List if all the types are the same.

  • defines record

The record construct is used to define structured data (an aggregate) much like a C struct, all field information held in the record is public see basics for details on visibility. But records can have operators, these are similar to methods but are limited in number and have very specific names and purpose.

Here is an example of a record, this is designed to hold some details about a person. As you would expect there are name details and a date of birth, but also a profession. Don't worry too much about the details of the operator statements they will be explained in the operators section. But an explanation of this and several other elements of syntax is given below (most of which is applicable to other constructs).

#!ek9
defines module introduction
  defines record
  
    Person
      first String: String()
      last String: String()
      dob Date: Date()
      profession String: String()
      
      operator ?
        <- rtn Boolean: first? and last? and dob? and profession?
      
      operator $
        <- rtn String: first + ", " + last + ", " + $dob + ", " + profession
      
      operator ==
        -> arg as Person
        <- rtn as Boolean
        rtn := arg? and first==arg.first and last==arg.last and dob==arg.dob   
//EOF

Let's consider:

  • first String: String()

A field/property called first is being declared and its type is String. Then a ':' this is one of the assignment operators it is also acceptable to use '=' or ':=', see operators on why there are three different symbols to accomplish the same thing. Finally there is String(), this creates a new instance of a String Object. But importantly the variable first is not set to "", it is actually un set; meaning it has memory allocated to the variable but the value is meaningless.

So declarations on aggregates in general follow the form of: {variable name} {type name} assignment {initial value}. You can omit assignment {initial value} and leave the field unset - but it is good practice to assign it (even if this value is something that does not yet have meaning - as in the example above).

Operators

Here is just a quick outline of how parameters are received by methods and operators. The declaration below just states that what follows in an operator for checking equality. For anyone familiar with C++ this is the same sort of idea.

  • operator ==
Parameters and returns

Following the above the declaration the syntax below is designed to indicate what the incoming parameter is. See basics on how multiple incoming parameters are declared. Note the use of '-' and '>' to denote the incoming parameter and the fact it is not within any (). This is quite different from most other programming languages.

  • arg as Person

After the incoming parameter, the return parameter is also declared. Note that if the processing is simple then the logic can actually be in line just after the declaration of the return variable. Or as in the case of the equality operator above it can be put into the body of the operator, so it could just have been written like this below.

  • rtn as Boolean: arg? and first == arg.first and last == arg.last and dob == arg.dob

As you have seen the ← is also used to indicate a return as well as being used to define and assign a new constant/variable. In general it is used in other contexts (such a ternary expressions) to mean 'coming out of' or 'returning a value'. The general idea of using → and ← is to give a very clear visual idea of data/information flow, either coming in or being returned/assigned from.

Incidentally the above statement also outlines '==' (equality) and a boolean expression using 'and' as you can imagine EK9 also supports 'or'. It does not use '&&' or '||' in the way Java, C, C# and C++ do. EK9 just uses 'and'/'or' even when working with Bits.

What's the ? for

You might be asking yourself whats the ? used for (as in the example below).

  • rtn as Boolean: arg? ...

It is the EK9 way of asking is this variable set (is set). In other languages this might be done as a null check. But in EK9 it is possible to have a variable that has either not been allocated any memory to hold a value or it has been allocated the space but that space does not really hold any meaningful value. Here is the declaration of some variables as an example.

  • name1 as String?
  • name2 as String := String()
  • name3 as String := "Some Value"
  • name4 as String := String("Some Value")
  •  
  • Inferred type declaration
  • name5 "Some Value"
  • name6 String()

The variable name1 has not been allocated space to store anything - so name1? will return false.

Variable name2 has been allocated space for storage but no meaningful value has been provided; so name2? will again return false.

Variables name3/name4 have been allocated space and have also been initialised and so name3?/name4? will return true. There is more detail on this in the basics section.

Variable name5? will return true, but name6? will return false.

Type Inference

It was mentioned earlier that EK9 has type inference, but other than constants shown above no inference has been shown in any of the examples; all the types have been explicitly declared.

In EK9 type inference is for the consumer of an API or component, the creator or producer of any component has to be explicit in what types they are using. So as a concrete example of this; the program below shows how a consumer of the record Person can benefit from type inference.

The developer creating the component/API etc always has to go the extra mile when creating something; they have to be explicit in the types that are accepted and those that are returned. The consumer on the other hand gets all the benefit in terms of both functionality provided and type inference. This approach tends to drive more thoughtful API development by more senior developers, whilst providing more junior developers a simpler coding experience - but this enables them to learn what good API should look like and hence improve.

#!ek9
defines module introduction    
  defines program
  
    InferenceExample()
    
      smithj <- Person()
      
      //Three different ways of assigning value
      smithj.first: "John"
      smithj.last := "Smith"
      smithj.dob = 1960-01-01
      smithj.profession: "IT"
      
      firstName <- smithj.first
//EOF

The consumer can just use ← to declare a new variable 'smithj' and assign it a new instance of Person; the type is inferred. The example above also shows the three different ways assignment can be made (to the fields/properties on the Person record), your own personal preference can be employed here as to which appears most readable to you. Section basic assignments contains the reasoning as to why there is alternate syntax for assignment.

Finally the firstName declaration shows how type inference can be used when accessing part of a structured (aggregate) data type.

As an aside, it has been noticed that the simple use of ← to both create a variable and have its type inferred leads to less reassignment of existing variables and a better naming of variables. This is a welcome side effect assuming that clarity of intention is valued higher that memory allocation and use; which in EK9 it is.

This discussion has digressed from being just an explanation of the record construct into declarations, assignment and type inference because more examples are needed for other constructs so the syntax needed to be at least outlined.

As an example of this; what type is the variable firstName in the example above? With the Person record being explicit in having to define the types the question can be answered quickly (String). While trivial in this case, you might imagine a long chain of function or method calls, navigating all the types in this scenario can become much involved.

Class
  • defines class

The class construct is also used to define structured data (an aggregate) but you can add methods and control their visibility (public, protected and private). Unlike the record, all field information held in the class is private; see basics for details on visibility. There is no concept of protected or any special secret, privileged or friend arrangement with fields/properties on classes. They are all private, information hiding and principle of least knowledge are applied here. This also means as a developer when defining any such aggregate the visibility of fields/properties is always decided for you (it is always private).

Below is an example of classes, some are abstract with some that can be extended and others that cannot. The Kotlin language has had an influence here on making classes not extendable unless indicated by open or abstract keyword and also enforcing override on methods. Some developers may see this as a very bad approach (closing classes by default), but it is aimed at driving solutions to use traits and composition more widely rather than just inheritance. The EK9 language has support specifically for composition.

#!ek9
defines module introduction
  defines class
  
    //If you say it is abstract then it is by default 'open' so it can be extended
    Base as abstract
      singleMethod()
        <- rtn String
        
    //This cannot be extended as it is not 'abstract' and not 'open'
    Extension1 extends Base
      //declaration and initialisation of a field/property
      aField as Boolean: true
    
      otherMethod()
        <- rtn String: $ aField
      
      //must provide this method as Base has declared it but not implemented it.  
      override singleMethod()
        <- rtn String: "Steve"
        
    //Because this is marked as 'open' it can be used and extended
    Extension2 extends Base as open
      override singleMethod()
        <- rtn String = "Steven"
        
    Extension3 extends Extension2
      override singleMethod()
        <- rtn String := "Stephen"    
//EOF

Here again you can see the return parameter being declared on a method. Note how the different assignment ':', '=' and ':=' operators have been employed to show what is possible. In general it is best to pick a style for a context and stick with it. For example; maybe ':' should always be used for return parameters, it does look quite terse and simple. But this developer choice.

The keyword override must be used when overriding methods, in EK9 all non private methods can be overridden by sub classes but it must be explicit when they are.

The sections on classes and methods provide much more detail on concepts such as extends, abstract and open. But with EK9 you can have as many or few classes in a single source file as you like. In general, grouping lots of small simple classes in a single file makes development much easier. But the converse is also true a single file with many large or diverse classes in the same file makes development harder.

Good 'style' would dictate that small classes that are in some way related may go into the same file. But if the file gets larger than about 500-1000 lines of code it is probably best to split them up. Remember they can still be all in the same module but in different source files.

The big disadvantage of not enforcing a naming convention of same source file name as construct name is navigation, but as developers you can and should come up with reasonable working practices/standards. You can then argue about it (as is our way).

A Non Aggregate Construct

As mentioned before functions are first class constructs in EK9, there are in fact several varieties of function; while they are not strictly aggregates, some types of function can hold state and have variables that live beyond their invocation (a little like a closure).

Function
  • defines function

The function construct is used to define an element of code that just performs an operation with optional parameters passed in and out. The concept is similar to a function in the C language, but there are some significant differences. An example is given below:

#!ek9
defines module introduction
  defines function
  
    //Just a function signature.
    stringFilter() as abstract    
      -> string as String
      <- rtn as Boolean
      
    nonColon() extends stringFilter()
      -> string as String
      <- rtn as Boolean: string <> ":"
        
    isSet() extends stringFilter()
      -> string as String
      <- rtn as Boolean: string?
        
  defines program
  
    TestFilter()
      stdout <- Stdout()
      toTest <- "Steve"
      
      if nonColon(toTest)
        stdout.println(toTest + " does not have a colon")
      else
        stdout.println(toTest + " does have a colon")
      
      //It is also possible via delegate access
      stringChecker as stringFilter := nonColon
      if stringChecker(toTest)
        stdout.println(toTest + " does not have a colon")
      
      //As a delegate we can swap the function out for another compatible one.
      stringChecker = isSet
      if not stringChecker(toTest)
        stdout.println("The value toTest is not set")        
//EOF

The example above defines an abstract function signature and also concrete functions that match that signature. It is not always necessary to define the abstract function if you don't need to treat it in a polymorphic manner. The program in the example shows how the function can be used in two different ways. The sections on functions and dynamic functions have a lot more detail on this.

The incoming and returning parameters on functions are defined in the same way as those described in the examples above using the ← and → respectively. When calling the function (or methods on classes) the parameters are passed inside ().

In the above example the function is called directly and also as a delegate, but note that because there is an abstract function and both of the concrete functions extend it - it is possible to use functions in an Object Oriented manner. The variable stringChecker is of type stringFilter and as such can be set to any type that is compatible with stringFilter. This means that it can be set to nonColon or isSet as both of these functions extend stringFilter. Note ':=' was used in this declaration but ':' or '=' could have been used (if you prefer one of those).

Indeed this also enables higher order functions see section functions for more detail on how these can be used in EK9. The concept of defining and using a function as an Object and then being able to use it in a delegated form is much more powerful than it immediately appears.

As stated before classes and functions are treated on equal footing in EK9 and they have influenced each other during the evolution of the language; dynamic functions and dynamic classes take this even further. But when combined with composition create many more ways to solve problems and facilitate re-use.

By providing strong typing and using the concept of abstract functions the simple function has been melded with an object orientated concept to provide type safe flexibility and function polymorphism. This simple concept is essential to a much more powerful functional concept of pipeline processing and lays the very fabric of what will be needed later.

Trait

A little like a Java Interface with default methods, but more a kin to the idea of a 'Trait' from Scala. Classes exhibit traits or characteristics. For example a Bird class might have the trait of 'Winged', Penguin might also have 'trait' of Swim, but not the 'trait' of Fly.

The word Trait is a much better construct name to indicate that some Class exhibits some type of behaviour but is not necessary 'IS A' type.

EK9 has taken the concept of a trait and added mechanisms to restrict and control classes that use it.

  • defines trait

This example is a little more detailed. It is designed to give you some idea of things that you can do in EK9 not many other languages support. It employs shapes to explain how traits work. A trait is similar to a class in many ways; it can be abstract in the sense it can define method signatures with no body or the methods can have full implementations (bodies). Traits can never be instantiated in their own right (unlike classes); but if the trait has all the methods implemented it is possible to create a dynamic class without the need to provide any additional method implementations. They can also extend other traits.

#!ek9
defines module introduction
  defines trait
  
    ShapeLimit allow only Shape, Ellipse, Circle, Rectangle, Square, Triangle
      printDrawing()
        -> message as String
        //method body
        stdout <- Stdout()
        stdout.println(message)

The above declaration defines a trait called ShapeLimit it has two main purposes. Firstly to provide a mechanism that can be used to just print out a message to the Stdout; secondly but more importantly it provides a mechanism to limit other traits and classes that are allowed to extend it. This is similar to the concept in Java of sealed classes. This is done with the following statement:

  • ShapeLimit allow only Shape, Ellipse, Circle, Rectangle, Square, Triangle

Shape and the other named classes are yet to be defined (see extension to this example later) but by using allow only; the types of traits and classes that can extend the ShapeLimit trait are controlled. When you develop a trait if you don't want to limit its use then just omit allow only and anything after it.

But there is a slight downside to using this, under normal circumstances the classes that implement the trait 'ShapeLimit' are clearly dependent upon 'ShapeLimit'; but the converse it not true.

By using allow only and a list of one or more traits and classes in our definition of 'ShapeLimit' we have now created a mutual dependency. Be sure this is really worth doing.

Continuing the example with an abstract class Shape and then some concrete classes.

...
  defines class
  
    Shape with trait of ShapeLimit as abstract
      draw() as abstract
                  
    Ellipse extends Shape as open
      override draw()
        printDrawing("Ellipse")            
    
    Circle is Ellipse
      override draw()
        printDrawing("Circle")                
    
    Rectangle is Shape
      override draw()
        printDrawing("Rectangle")                
    
    Triangle is Shape
      override draw()
        printDrawing("Triangle")            
    
    Square is Shape
      override draw()
        printDrawing("Square")        
//EOF

The use of the ShapeLimit trait is explained below.

  • Shape with trait of ShapeLimit as abstract
  • //Could have been written in shorthand like this
  • Shape of ShapeLimit abstract

This declares a new class that is open to be extended by other classes, but the Shape itself is abstract; meaning that it itself cannot be instantiated (with a statement like Shape()). It also adds a new abstract method called draw() that must be implemented by any non abstract sub class. If you've never developed in an Object Oriented methodology before it might be worth looking at Wikipedia or other resources first.

Incidentally, you may have noticed as is used here and there, this is an optional bit of syntax that sometimes makes the code easier to read. In addition, for extension you can use either extends or is it has the same meaning, but again sometimes make the code more readable.

As you can see from the statement below it is also possible to ensure that other classes are open. By default they are not open and therefore cannot be extended.

  • Ellipse is Shape as open
  • //This could have been written like this
  • Ellipse extends Shape as open

The above full example shows a number of classes in a hierarchy that are being controlled by a trait, the trait itself is providing some functionality in method printDrawing and also constrains and limits what other traits and classes it will allow. If it had also defined an abstract method then it would have also forced concrete classes to implement that method.

The trait is really a very controlling and very powerful construct. It has even more capability when used with composition.

Traits really can provide the 'glue' via abstraction to link/control classes together is various ways. So don't think of them as 'inferior' to classes or little more than an abstract class; they are much more than that.

More advanced features/constructs

Depending on your knowledge, experience and use of other languages you may feel that what has been outlined so far is just a merger of ideas and mechanisms from other languages (but with a different syntax). To some extent this is true, but some small amounts of additional capability has been added.

If on the other hand you are new to software development and all the above concepts are new to you; it would probably be best to spend quite a bit of time using what you've learned so far; as the next sections get more advanced.

Generics/Templated types

Generics or Parametric polymorphism as it is more formally known, can be applied to both functions and classes in EK9; examples of which are shown below.

Generics (Functions)

Generic/Templated programming is the concept of defining some functionality irrespective of type. EK9 has some capabilities in this area and while not being as fully featured as some languages - it does provide some useful functionality.

To understand how generic functions can be employed it is necessary to understand dynamic functions these are quickly covered here below in the context of explaining generics.

#!ek9
defines module introduction
  defines function
  
    compareFunction() of type T
      ->
        item1 as T
        item2 as T
      <-
        rtn as Integer: item1 <=> item2  

  defines program
  
    Comparator()
      stdout <- Stdout()
      intComparator <- () is compareFunction of Integer
      stringComparator <- () extends compareFunction of String
      
      intResult <- intComparator(2, 8)
      stdout.println(`Result of int comparison is ${intResult}`)

      stringResult <- stringComparator("two", "eight")
      stdout.println(`Result of string comparison is ${stringResult}`)

//EOF

In the example above the result output to stdout has been defined using Interpolated Strings

There are a few new bits of syntax that have not been covered before.

  • compareFunction() of type T

Functions have been discussed before but this function has the additional syntax to indicate it uses a type called T. Really T is just a way of stating the fact that a type is going to be referenced later in this function and it refers not to something real like a String but it could be anything at all. You can use any name here S, K, V (convention is a single uppercase character).

Then there is the declaration of two incoming parameters notice how the → is on a line by itself and then the two incoming parameters are indented before being declared - this is how you declare multiple incoming parameters, see parameter passing for more details.

  • item1 as T
  • item2 as T

Within the function body standard operators on the notional type T are assumed. This means all operators can be used with the type T; if and when you actually declare you're are going to use this generic function with a specific type - only then are those operators checked on the type you intend to use.

Incidentally the comparison (spaceship) operator '<=>' is used in the return statement of the function compareFunction:

  • rtn as Integer: item1 <=> item2

If you've used languages like C++ or Java where < > are used in generics/templated types; please remember EK9 does not use that syntax. The '<=>' is use to compare to objects (and you can override this operator when defining your own records and classes.

In the program example above the generic function is (re)used twice.

  • intComparator() is compareFunction of Integer
  • stringComparator() extends compareFunction of String

The above declaration is just one of the forms of dynamic functions, there are other forms that can be applied to non-generic functions. These declarations are basically stating that you intend to take the generic function compareFunction and use it with an Integer but also a String.

In concept it's like taking a prebuilt bit of code as a generic template and saying just replace T with Integer or String then do a quick check to make sure that any operators used are present on the type I've chosen (in this case Integer and String).

  • intResultintComparator(2, 8)
  • stringResultstringComparator("two", "eight")

The two new functions that have now been given a type can be called and are strongly typed, only accepting Integers or Strings respectively.

There are ways to constrain T to be limited to specific types and there are more details on generic functions in section generics/templates. But this should give and idea of what the concept is and why it is useful. The same/similar idea is outlined below for generic classes.

Generics (Classes)

The example below uses a Finite State Machine to show the combination of generic functions and classes and a slightly different use of dynamic functions where the function body is actually overridden. This example is much longer and aims to give you more of a feel of how functions, classes with generics can be used in combination.

Please note that in the EK9 language you can reference something like FSM below before it is even defined. This may at first seem strange but EK9 uses a multi-pass mechanism to enable this to happen.

#!ek9
defines module introduction
  defines type
  
    FSM of Integer    
    
  defines function
  
    FSMListener() of type T
      ->
        fromState as T
        toState as T        
      assert fromState? and toState?
      
  defines class
  
    FSM of type T
      currentState as T
      listener as FSMListener of T      
      
      FSM()
        -> initialState as T          
        assert initialState?
        currentState := initialState
        
      setState()
        -> newState as T
        assert newState?
        if listener?
          listener(currentState, newState)
        currentState := newState   
      
      getState()
        <- rtn as T: currentState       
      
      setListener()
        -> listenerToAdd as FSMListener of T
        assert listenerToAdd?
        listener := listenerToAdd
        
  defines program
  
    Driver as    
      myListener <- () is FSMListener of Integer
        stdout <- Stdout()
        assert fromState? and toState?
        stdout.println(`Changing from ${fromState} to ${toState}`)
      
      myMachine <- FSM(2)      
      myMachine.setListener(myListener)
      myMachine.setState(5)               
//EOF

There are quite a few new bits of syntax in the example above, these are explained below.

  • defines type

This was discussed before when explaining enumerations, but it can also be using in the context to forward declare a generic class being used with a specific type, as below.

  • FSM of Integer

The generic function FSMListener should be obvious if you've read the previous section. The new concept here is the generic class called FSM of type T. It has a couple of fields/properties (these are private).

  • currentState as T
  • listener as FSMListener as T

The rest of the methods are fairly obvious and also work with the notional type T. The Driver program then uses a dynamic function to act as a listener. In this case not only does it use the type (Integer) it also overrides the functionality of the generic function FSMListener. This implementation then checks the states are valid and then just prints out the fact that the state is changing from one value to the next.

  • myListener ← () is FSMListener of Integer
  •   stdoutStdout()
  •   assert fromState ? and toState ?
  •   stdout.println( `Changing from ${fromState} to ${toState}` )

It was not necessary to redeclare the incoming parameters fromState and toState they can just be used when overriding the function body.

So there's quite a bit going on here, first a generic function has been defined, then that function has been used in a polymorphic parameterised manner; finally that function has had its functionality overridden like it was an class in an Object Oriented methodology to provide new functionality.

The next interesting part is how the generic class is used with type inference.

  • myMachine FSM(2)

Because 'FSM of Integer' was forward declared, EK9 can work out that 'myMachine' is of type 'FSM of Integer' by the fact that the constructor parameter to the FSM is an Integer. Finally the listener is set and then a call to set the state to '5' is made and the listener will be triggered.

This is the end of the generic programming section here but there is more detail in section generics/templates.

By using the listener approach and a generic finite state machine it is possible to use the same code/logic and listeners in a very re-usable manner. There could be any number of listeners for a range of different types and with dynamic functions you can just provide the functionality you want.

Component

A component is much like a class. Again all fields/properties are private; but significantly all methods are either private or public. The main point of the component is to model something that is a significant concept/singleton in an overall solution and importantly can be injected into other constructs such as classes and functions. The injection part is accomplished through the creation of some form of abstract base component. This is then extended to provide a concrete implementation.

Components are designed to provide the developer with a way to limit coupling between modules and sub systems, by providing a much less granular software construct so that details have be encapsulated. So it's important not to let low level details of how the component is implemented 'bleed out'. This is the main reason all Exceptions are unchecked in EK9. Try and hide as much of your implementations behind a component where possible; as it really improves the opportunity to plug in new implementations, refactor and isolate your code.

This example below shows three different implementations for a 'Base Configuration'. A good use of this might be during testing or development where you'd like to use one implementation and then another in production. Or maybe you will be planning to transition from one area of storage to another when the next version of this application is deployed. But all the classes and functions need to know what the store is called.

It is also possible that you might want to have a range of implementations available and only select the right major component building blocks once the final solution/configuration has been decided upon. This suits companies that build core sets of components and then; in discussion with their client, decide upon the right combination of existing or new components to employ.

#!ek9
defines module introduction
  defines component
  
    BaseConfiguration abstract
      getFileStoreName() abstract
        <- rtn as String
        
    MainStoreSolution is BaseConfiguration        
      override getFileStoreName()
        <- rtn String: "MainStore"
    
    SecondaryStoreSolution extends BaseConfiguration
      override getFileStoreName()
        <- rtn as String := "SecondaryStore"
        
    DefaultStoreSolution extends BaseConfiguration
      override getFileStoreName()
        <- rtn as String := "DefaultStore"
...              

Obviously there could be any number of methods on the components, load, save etc. But to continue with this example, the appropriate 'Base Configuration' needs to be selected and 'wired in'. Normally an application will be made up of many different components. For example here we have the 'Base Configuration' for storage, but there could be other components for various 'Service Connectors' or other linkages to other systems that need to be modelled as components. See application below on how a component is selected for use.

There is a more more detailed example in the components section.

Application

An application is a configuration of one or more components that have been selected. So it is quite possible/desirable to create a library of classes, functions and traits etc as parts that could be used. Then to compose those parts into components. Now an application can be composed of those components. That too is just added to the library but is clearly a major and significant item.

The focus on components in EK9 is really around fairly large 'lumps' of a system that make use of several classes, functions or other components. An example could be CustomerStorage, this could be designed to deal with all sorts of 'customer' related information. By wrapping this layer up as a component it could be replaced with alternative storage solutions at a later date.

Continuing from the example above the code below shows three such applications to demonstrate this idea. Typically an application would register many different components for different parts of the functionality. So App1 and AP2 use 'Redis' for caching but App3 employs 'Memcached' (as an example).

But please note; the idea is not to build large monolithic applications. That will lead to:

Once your software gets passed a certain size (and rules of thumb and experience are needed here), you really have to move to micro-services even in a desktop application. If Microsoft can do it with the Language Server Protocol and AWS do it with almost all of their services it clearly has some merit.

But you have to architect the solution - and don't leave that too late. I know agile is the 'thing' at present - but you still must plan an overall design and focus on coupling and cohesion.

...
  defines application
  
    AppV1
      register MainStoreSolution() as BaseConfiguration
      register RedisCache() as DataCache 
    
    App2
      register SecondaryStoreSolution() as BaseConfiguration
      register RedisCache() as DataCache
      
    App3
      register DefaultStoreSolution() as BaseConfiguration 
      register MemCached() as DataCache
...

An analogy here would be car manufacturing and use. There will be many parts used across a range of vehicles, those smaller parts (bearings, gears, valves, etc) will be combined together to make subsystems (such as the engine or gearbox for example). Then those subsystems are selected to go into a range of different models of car. The cars then are made ready to be shipped to the distributor for sale.

Only when the vehicle is sold does the owner drive it away and use it. This is done in EK9 by the program construct which has been outlined before, but now an additional bit of syntax is used to decide which application is employed (much like the selection of car to buy and use).

So in the scenario above why have 'MainStoreSolution', 'SecondaryStoreSolution' and 'DefaultStoreSolution' ? As a concrete example of what you may need to do (based on experience): migrate from an existing implementation (say 'MainStoreSolution') to a more high performance implementation (say 'SecondaryStoreSolution'). So why have 'DefaultStoreSolution' at all - well may be you want to hedge your bets and have another solution ready just in case 'SecondaryStoreSolution' does not work out, or perhaps it has a lower cost at runtime.

If you are new to development; you may think the above scenario is contrived, if you have any experience you'll know that preparing for this type of issue from the outset in large long lived software solutions is essential. The larger the software solution the more important this is. This is also one of the main reasons you should consider using components in software that is above a certain size (but don't adopt components too soon or too late).

Once you start to feel you have too many components then start thinking of 'micro services'. If you have wrapped your components up nicely; refactoring into 'micro-service' will be easier. You will see a drop off in 'snappiness' of the application as you now have to deal with 'marshalling' and network 'latency'. But you'll gain other benefits in terms of software maintainability, development agility and higher scaling/robustness.

Components and Packages together with module naming is the key to formalising interfaces that can be turned into 'micro services'. If you always design with these three things in mind then you will have hedged your bets if the software starts to really get large.

Returning to the car manufacturing analogy, designing a vehicle to standards (bearing sizes, connectors, wiring capacity) enables alternate/additional components to be used, those bearings with higher performance, more load bearing, higher speeds. Additional electrical loading for navigation, automation and sensors. These new innovations can be incorporated; if the initial design 'envisages' some future change or improvement.

...
  defines class
  
    ConfigHandler
      //Because BaseConfiguration is an abstract component it will get injected
      config BaseConfiguration!
      showConfigDetails()
        stdout <- Stdout()
        stdout.println(config.getFileStoreName())
    
  defines program
   
    v1Program with application of AppV1
      configHandler <- ConfigHandler()
      configHandler.showConfigDetails()
          
    v2Program with application of AppV2
      configHandler <- ConfigHandler()
      configHandler.showConfigDetails()
      
    v3Program with application of AppV3
      configHandler <- ConfigHandler()
      configHandler.showConfigDetails()
//EOF

So if the EK9 code was stored in a file called configtest.ek9 then each of the programs could be executed in the following way.

  • $ ./configtest.ek9 -r v1Program
  • $ ./configtest.ek9 -r v2Program
  • $ ./configtest.ek9 -r v3Program

Now the user can decide if they will use 'v1Program', 'v2Program' or 'v3Program' with the associated application that goes with that. You may be asking where's the big advantage in all this?

Imagine that the 'Base Configuration' needs to be used in more than just the simple class ConfigHandler but needs to be employed in many classes and functions. Now to swap the 'Base Configuration' out normally you'd have to find all the references to say 'MainStoreSolution' and change them all to 'DefaultStoreSolution' (for example). Now all the classes and functions use 'DefaultStoreSolution' but you've had to modify all that code.

This is where Inversion of Control comes in. It is the use of 'v3Program' with the configuration of 'AppV3' that decides that 'DefaultStoreSolution' component should be used. But all of the classes and functions still need access to this selected configuration; that's where Dependency Injection is employed. By declaring the use of a component 'config BaseConfiguration'; the ConfigHandler is in effect asking for that component to be injected, it has no knowledge of what actual implementation will be provided (its given away control).

Aspect Oriented Programming

The snip of code below gives you an early view of AOP as the application registration of components is also the mechanism that is used to define the aspects that can be intercepted for AOP purposes.

  • register MainStoreSolution() as BaseConfiguration with aspect of TimerAspect(), LoggingAspect("DEBUG")

In short the above configuration would wrap the calls that are to be made on the methods of the component 'MainStoreSolution' with specific AOP method calls on classes TimerAspect and LoggingAspect (not shown in detail). This means that every time method getFileStoreName() (or any other methods on that component) get called they are intercepted and methods beforeAdvice() and afterAdvice() are called on the aspects configured.

Why do this? In general aspects are really useful for Cross Cutting Concerns. This means that should you decide your really wanted to log all calls on a component or may be time how long a component took to complete some processing; you can leave the component as is and just use AOP to intercept the called.

This is really useful for:

The biggest advantage of all is that a component does not have any additional logging or other code in it. It only needs to contain the actual functionality needed. Also the logging code itself when written as an aspect class is fully reusable over all components. So you have the advantages of Separation of Concerns and Reuse this leads to more maintainable code.

This is getting quite advanced here, the components, applications and aspects are really useful for larger software projects. For simple and small projects they are generally not needed. But once you start to get to any reasonable scale they really become essential.

Only use these tools when you need to. Like any technique it's always best to use it in the right context. EK9 is designed to go from very small programs right up to very large scale solutions; this is the main reason these design patterns have been incorporated right into the language (it is hard to develop reliable, reusable and robust software at scale without them).

If you find you have a need to wrap logging, transaction boundaries or want to time how long a web service takes from being called to returning a response, then look to components and aspects. You can write one lot of logging code )for example) and just (re)use that over and over again irrespective of the components you are applying it to.

  • register SimpleDevelopedSecurityProvider() as isolated SecurityProvider

By adopting the DI/IOC approach EK9 enables the developer to define any number of abstract security privileges to be used throughout the application, but importantly it does not link these directly to any sort of real concrete provider. This enables software to be tested and developed without the need for real calls to LDAP (for example) during unit test and automated tests. It also enables a range of error conditions to be tested by using different SecurityProvider implementations.

Text

In the previous examples fixed text in quotes have been used for outputting information. In general many applications need to support several spoken languages, typically users are given a number of options for the language that they might like information and messages to be shown in (i.e. a Locale). In the example below set of localised information is bound into a 'text' object called ProductsText. These 'text' objects are similar to classes but with a focus on pure text and support string interpolation, via method calls that can accept parameters.

The important point about 'text' objects is that they are compiled, they are not just a set of property files. The parameters and the interpolation of strings using those parameters are checked.

#!ek9
defines module introduction

  defines text for "en"
     
    //Define some text in English and wrap in a 'text' object.
    ProductsText
      productsTitle
        "Products"
      productFineDetails()
        "The fine details"
      share
        "Share"
      moreDetailsOn()
        -> product as Product
        `More details on ${product.name}`
        
  defines program
  
    TestText
      stdout <- Stdout()
      text <- ProductsText("en")
      
      //Details of the Product record omitted for brevity
      specialProduct <- Product("Special")
      stdout.println(text.productsTitle())
      stdout.println(text.moreDetailsOn(specialProduct))
      
//EOF

The statement below is from the above example, the rest of the 'text' definition should be fairly obvious by now (assuming you've read earlier parts of this page). As text methods only ever return Strings no return value is specified incoming parameters are optional (i.e. you can just have pure text). The () are optional just like they are when defining functions and methods.

  • moreDetailsOn()
  •   → product as Product
  •   `More details on ${ product.name }`

In this case the parameter product passed in can be any sort of type, in this case it is a Product which is a record. The field name on the record is public and so it can be directly accessed. Note the '${' this denotes the start of the interpolation, the '}' denotes the end of the interpolation. The entire String is then returned once the interpolation is complete. The Interpolated String starts and ends with a back tick `.

Package

The package construct is really key to enabling the use of other "dependencies" (packages), It enables you to decide on what source code to include in the compile and if any version number should be associated with the software you have created. There is much more detail on how to do this in the Packaging section.

But unlike a lot of other languages that use external packaging and version management; EK9 includes it in the language itself. There is no need for external xml/json/yml files for configuration, just use the language you are writing code in!

Finally edge services

All of the constructs so far have either been normal programming language concepts or design patterns (DI/IOC - Aspects etc). The last two constructs really draw modern web based development into the language itself.

As explained in some detail in the applications section - once components get to the point where your application has become sufficiently large - you need to move to 'micro services'. The section that follows provides the elements needed to be able create and use 'micro-services'.

Service

Web Services were outlined in the introduction but there are a some more details here. For full details view the section on Web Services. This construct is designed to make applications/components accessible via HTTP/REST/RESTful web services. As such it uses Object Oriented mechanisms that are applicable to limit the amount of new terminology needed by developers. There are some aspects of web services that really cannot and must not be hidden. EK9 does its best to find a balance.

#!ek9
defines module introduction
  defines service
  
    Addresses :/addresses

      byId() :/{address-id}
        ->
          addressId as String :=: PATH "address-id" //required because different name
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      listByPostCode() :/{postCode}/
        ->
          postCode as String //Now assume PATH PARAM because there is a param
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      list() :/
        ->
          postCode as String :=: QUERY
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      //Basically a POST    
      operator + :/
        ->
          request as HTTPRequest :=: REQUEST
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      //A DELETE
      operator - :/{addressId}
        ->
          addressId as String
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      //PUT
      operator := :/{addressId}
        ->
          addressId as String //assume PATH
          request as HTTPRequest :=:REQUEST
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc

      //merge is a PATCH
      operator :~: :/{addressId}
        ->
          addressId as String //assume PATH
          request as HTTPRequest :=: REQUEST
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
    
    //Here is customer service (only partial)
    //But shows verbs 'GET', 'PUT' etc    
    Customers for :/customers

      //Just get by customer identifier
      //i.e http(s):somehostorother.com/customers/CUST-ABCD
      byId() as GET for :/{customerId}
        ->
          customerId as String :=: PATH
          acceptLanguage as Vary :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
      
      operator := as PUT for :/{customerId}
        ->
          customerId as String :=: PATH
          request as HTTPRequest :=: REQUEST
          acceptLanguage as Vary :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: () of HTTPResponse
            //Override and implement content() and status() etc
            businessProcess as BusinessProcess! // this would get injected as a component
            //Now use the business process and alter the response.
  
  //Application that exposes the web services to be made available.
  defines service application
    
    CustomerAccess
      //Register the business component (not shown in this example for brevity)
      register StandardBusinessProcess() as BusinessProcess
      
      //Now register the services to be made available.
      register Customers()
      register Addresses()
  
  //A program that starts up the REST server with the configuration above
  defines program
    
    RestWebApp with application of CustomerAccess
      stdout <- Stdout()
      
      //Run on socket port 9092 for this service.
      httpServer <- HTTPServer(9092)
      stdout.println("Now starting REST Services")
      httpServer.start()
//EOF

The following syntax defines a service that is mapped to a specific URL.

  • Addresses :/addresses

This ensures that the URL such as https://www.somehost.com/addresses is serviced by Addresses. For web services a key component is caching how long a response should be retained without re-requesting. The HTTPResponse trait must be implemented to send a response back to the caller. This trait has more methods than just content() and status; it also provides key methods to support caching:

There are other methods, but these above are the key to effective HTTP caching.

In general services will/should do very little processing, they should delegate most of the actual processing to a component. In the example show below a 'businessProcess' is injected by DI/IOC. The service layer should really focus on populating the HTTPResponse with sufficient data and specifically concentrate on supporting caching. It must also interrogate the HTTP headers to ensure it processes the request in the 'best way' it can. HTTP headers are full of 'hints' and options; such as Accept-Language etc. These should be used to provide the best type of response possible.

  • // This would get injected as a component
  • businessProcess as BusinessProcess !

The HTTP/REST method below shows how an address can be retrieved via a GET.

  • // Here the address-id is a path parameter that the service would use
  • byId() :/{address-id}
  • addressId as String :=: PATH "address-id"

The parameter address-id is not a valid EK9 variable name and indeed HTTP parameters in general are not EK9 variable name compatible. It is therefore necessary to copy (:=:) the HTTP parameter into a valid EK9 method parameter. The location from where the variable is copied can be PATH, HEADER, QUERY, CONTENT or REQUEST.

  • // The body in some cases is also required, this is shown below.
  • request as HTTPRequest :=: REQUEST

The general mechanism in web services is aimed at mapping HTTP parameters into incoming method parameters and this includes coercing incoming "String" values into strong types. Again defining the service has the effect of adding a service to a library of functionality, however it is only when those services are registered via an application do those services become effective. Subsequently a program can be used to start and run the application CustomerAccess in this case.

The EK9 language does not try to mask or hide the HTTP protocol, but makes mapping the protocols into mechanisms that are similar to EK9 component/class constructs. EK9 then reuses the component and application constructs to deploy the web services defined. It is really aimed at getting the data/process being triggered via REST into strong types as soon as possible and then facilitating the use of components to do the actual processing. In other words the construct is purely designed to be an adapter but with real focus on caching and different representations in responses.

REST/RESTful Summary

The minute you move from a single executable application and employ 'micro services' is the time when you need to have embraced 'devops'. You are now in the realm of the 'network engineer'. You really need to leverage http caching, containers, rigorous build pipelines and rolling automated deployments. Ideally you will have already done this with your monolithic application to some degree. But the level of rigor around maintaining public interface compatibility and planning for mass rollouts of new software that breaks that compatibility take coordination and planning. The section on web services discusses this further.

Summary

This section on 'structure' has been designed to give an overview of all the constructs that EK9 has to offer. Just to recap the main aspects.

Hopefully you will be interested to learn much more and use EK9; it is a big language. Remember you can use just parts of it if you wish.

Next Steps

To learn more in fine detail on the language itself rather than just its structure take a look at the basics next. You can use the navigator bar to the left to jump around. Hopefully by now with the few examples given you will have got the gist of EK9. Now for the fine details of variables and control statements.