Structure of EK9

An EK9 source file has a .ek9 extension, no spaces in its name, and #!ek9 as its first line. After that, a file has this form:

The Constructs

Where to start. You don't need every construct to begin.

EK9 is a tool box. It scales from one program that parses a JSON, XML or CSV file to a full system of microservices, but you only take out the tools you need. Leave the pliers (components) in the box until you need them; programs and functions alone, as in C, are enough for many jobs. If you build a full-stack application with microservices, you will meet the same kinds of APIs and ideas even if you use third-party tools or frameworks. Each section stops at a point where what you've learned is usable.

To see some EK9 first, look at hello world or jump to the program below. The later constructs assume more about enterprise-scale development; where they use ideas such as functional programming, object-oriented programming, aspect-oriented programming, inversion of control and dependency injection, this page links to outside reading. There is also a post on the functional and object-oriented features a language needs.

Important: Indentation-Based Syntax - No Semicolons

EK9 uses indentation to structure code, as Python does. There are no semicolons; a semicolon is error E01074.

Fails with E01074: semicolons are not needed in EK9 - uses indentation-based syntax like Python

#!ek9
defines module bad.site.structure.semicolon

  defines program

    Greeting()
      name <- "Steve";
      Stdout().println(name)

End each statement with a new line:

#!ek9
defines module site.structure.nosemicolon

  defines program

    Greeting()
      name <- "Steve"
      Stdout().println(name)

Set your editor to indent with two spaces, not tabs. The EK9 IDE (ek9 -ide) does this for you: Tab inserts two spaces and Shift+Tab outdents. For VS Code there is an EK9 extension that uses the language server (ek9 -ls). Excluded Features explains why.

Getting started with EK9 file layout

Module
#!ek9
defines module site.structure.layout

A module is a name space that holds constructs. The example uses site.structure.layout; a hierarchical name such as com.system.introduction works too. The parts of the name are separated by '.'. By convention a module name is lower case, though the compiler does not require it. Choose the name with care if you will publish to the EK9 repository, or reuse the code internally as a versioned release; see the package directive.

Any number of source files can use the same module name space, and every construct in them can see the others.

Names that share a prefix are not linked: com.p1.submodule is fully separate from com.p1, with no implied or secret access between them.

Why EK9 does it this way. To isolate software fully, use microservices: they can be isolated, managed and scaled independently. EK9 does not build 'Chinese walls' inside one application with class loaders or module hierarchies.

References

To use constructs from another name space, list them as references after the module declaration and before the first construct; see references. Each reference is fully qualified: there are no wildcards. A good practice is one source file in your module that holds all its references.

Why EK9 does it this way. With many links to other modules, listing them all is painful, and it is designed to be. The pain is there to reduce coupling between modules: to encourage encapsulation and information hiding, and to limit cyclomatic complexity. If you have too many references, refactor and start to use components.

The Constructs

After the module declaration come the constructs: the major building blocks, such as a function or a class.

A defines line opens a section, and every declaration after it is of that kind of construct; you don't repeat the kind on each declaration. This is like the sections of a Pascal source file that define constants or functions. EK9 lets you repeat sections in any order, but that is poor style.

Constant

You don't have to define any constants, but it is good style to put them all in one block near the top of a source file. The convention comes from other languages, and it shows other developers the constants first.

Constants can only be literal values of the basic built-in types (not a List or Dict); your own classes cannot be constants. To use your own classes in a similar way, see Components and Applications and Dependency Injection.

In EK9 a constant is for a real-world primitive idea such as PI, and it cannot be calculated: given PI and radius, you cannot define a third constant area as PI*radius^2, because that is an expression. Write a function instead. All constants are public.

#!ek9
defines module introduction

  defines constant

After the constant construct declaration, declare each constant:

PI <- 3.142    
e <- 2.71828

<- (typed as '<' then '-') declares a new constant and gives it a value; a constant is immutable. The type is inferred, so PI is a Float with the value 3.142. Variables use <- in the same way (shown later). There is no let, def, const or final.

Definition of two constants

#!ek9
defines module site.structure.constants

  defines constant
    PI <- 3.142    
    e <- 2.71828

Type

As well as constants you can define types; by convention they come after the constants.

A type is more than the enumeration shown below: constraints, aliases and generic forward declarations are all type constructs.

One section can define any number of types in succession; they don't need a source file each, as they would in Java.

defines type

This enumerated type can take four values:

#!ek9
defines module site.structure.cardsuit

  defines type
    CardSuit
      Hearts
      Diamonds
      Clubs
      Spades

An enumeration looks very simple (almost primitive, a little like YAML), but it is strongly typed, like Enumerations in other languages: a variable of type CardSuit can hold only one of the listed values.

Enumerations come with built-in operators that you don't write. $, for example, converts a value to a String: Hearts becomes "Hearts". You cannot add methods to an enumeration, as some languages allow; use a class for that.

Why EK9 does it this way. EK9 deliberately limits what each construct can do, to steer you to a more suitable one: constants are limited to steer you to components, and enumerations to steer you to classes. This is a theme across EK9. When a construct lacks a capability you think it should have, it is time to look at another construct.

Program

defines program

A program is the only way to start any application. One section can define any number of programs, and a file that needs no constants or types can hold only programs.

HelloWorld()

A program declaration looks like a method or a function, and in a way it is one. Here is the 'hello world' from the introduction again, now with two programs and a constant. Keep the programs you develop together like this; you choose which one to run on the command line.

#!ek9
defines module site.structure.hello

  defines constant
    worldMessage <- "Hello, World"

  defines program
    HelloWorld()
      stdout <- Stdout()
      stdout.println(worldMessage)

    HelloMars()
      stdout <- Stdout()
      stdout.println("Hello, Mars")

If the file is called helloworlds.ek9, run each program by naming it:

$ 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

Programs covers passing command-line parameters into a program.

To try things out now, write a program and add if/else/switch from flow control. When the code gets long, or you want to reuse parts of it in other programs, break it into functions.

Small, simple jobs are easy in EK9. A program that loads a text file and produces some output from it needs no classes or generics.

Record

The record is the first and simplest EK9 aggregate. Tuples exist in EK9 too, as dynamic classes.

defines record

A record holds structured data (an aggregate), much like a C struct. All its fields are public; see basics on visibility. A record can have operators: like methods, but limited in number, with fixed names or symbols and fixed meanings.

This record holds details of a person: name details, a date of birth and a profession. The operators section explains the operator declarations; the syntax notes below apply to most other constructs too.

defines record
  Person
    first as String: String()
    last <- String()
    dob <- Date()
    profession <- String()

    override operator ? as pure
      <- rtn as Boolean: first? and last? and dob? and profession?

    operator $ as pure
      <- rtn as String: `${first}, ${last}, ${dob}, ${profession}`

    operator == as pure
      -> other as Person
      <- rtn as Boolean: other? and first == other.first and last == other.last and dob == other.dob

    operator #? as pure
      <- rtn as Integer: #? first + #? last + #? dob

Consider:

first as String: String()

This declares a field first of type String. ':' is an assignment operator; '=' and ':=' also work, and operators explains why there are three. String() creates a new String object, but first is not "": it is unset, an object whose value has no meaning yet.

So a declaration on an aggregate has the form {variable name} as {type name} assignment {initial value}. You can leave out the assignment and {initial value}, but it is good practice to give one, even a value with no meaning yet, as here.

The properties last, dob and profession use the shorter type inferred form with a new, unset instance. It means the same.

So first could be declared in any of these ways:

first as String: String()
first as String := String()
first as String = String()
first <- String()

A variable or field that holds an object whose value is unspecified is an important idea in EK9: there is an object, but its value is not yet known. Many EK9 operators and processes are designed to work with it.

Operators

A quick outline of how methods and operators receive parameters. This declaration says that an operator for checking equality follows; C++ developers will know the idea.

operator ==

Parameters and returns

The incoming parameter comes next, marked with '->' and not inside (), which is unlike most other languages. See basics for declaring several parameters.

-> other as Person

A return value can be declared after the incoming parameter. Simple logic can go on the same line as the return declaration, or in the body of the operator, as here:

<- rtn as Boolean: other? and first == other.first and last == other.last and dob == other.dob

So <- marks a return as well as declaring and assigning a new constant or variable. In other places, such as ternary expressions, it also means 'coming out of' or 'returning a value'. -> and <- show the flow of data: coming in, or being returned or assigned.

Why EK9 does it this way. Visual layout matters in EK9. Its ideograms, indentation, columns and nested structures are designed to help spatial processing.

The operator also shows '==' (equality) and a boolean expression with 'and'; EK9 has 'or' too. EK9 does not use '&&' or '||' as Java, C, C# and C++ do; it uses 'and'/'or', even with Bits.

What's the ? for

The ? in this line asks whether a variable is set:

<- rtn as Boolean: other? ...

Other languages would use a null check here. In EK9 a variable can hold no object yet, or hold an object that has no meaningful value. Some declarations:

name1 as String?
name2 as String := String()
name3 as String := "Some Value"
name4 as String := String("Some Value")

//Inferred type declaration
name5 <- "Another Value"
name6 <- String()
Type Inference

Declarations, assignment and type inference covers each form in detail.

In EK9, type inference is for the consumer of an API or component; its creator must be explicit about the types it uses. This program is a consumer of the record Person and benefits from inference.

Why EK9 does it this way. The developer who creates a component or API always goes the extra mile and states the types it accepts and returns. The consumer gets both the functionality and the inference. This tends to drive more thoughtful API design by more senior developers, and gives more junior developers simpler code to write.

defines program

  InferenceExample()
    smithj <- Person()

    //Different ways of assigning value
    smithj.first: "John"
    smithj.last := "Smith"
    smithj.dob = 1960-01-01
    smithj.profession: "IT"

    firstName <- smithj.first
    Stdout().println(`${firstName} - ${smithj}`)

The consumer declares 'smithj' with <- and a new Person; the type is inferred. The example also assigns the fields of Person in all three ways; use whichever reads best to you. Basic assignments explains why there are three. The firstName declaration shows type inference when reading part of a structured (aggregate) type.

If 'Person' also declared a constructor taking all four details (a record is not given one automatically), it could be created more concisely. Four or more arguments must be named (E11062). This constructor gives each field its own copy of the argument:

#!ek9
defines module site.structure.personnamed

  defines record

    Person
      first as String: String()
      last <- String()
      dob <- Date()
      profession <- String()

      default Person()

      Person()
        ->
          first as String
          last as String
          dob as Date
          profession as String
        //Each field gets its own copy, so the caller's objects stay the caller's
        this.first: String(first)
        this.last: String(last)
        this.dob: Date(dob)
        this.profession: String(profession)

      override operator ? as pure
        <- rtn as Boolean: first? and last? and dob? and profession?

      operator $ as pure
        <- rtn as String: `${first}, ${last}, ${dob}, ${profession}`

  defines program

    NamedArguments()
      stdout <- Stdout()

      smithj <- Person(first: "John", last: "Smith", dob: 1960-01-01, profession: "IT")

      stdout.println(smithj)
Sharing or copying an argument

An argument is passed by reference: the constructor receives the caller's object, not a copy of it. Store it with ':' (this.first: first) and the record keeps a reference to the caller's object, so a caller that still holds it and changes it in place (firstName += " Jr") changes the record too. Two forms give the record its own value:

This record uses one form per field. After the caller changes all three of its Strings, only 'first', stored with ':', has changed in the record:

#!ek9
defines module site.structure.personalias

  defines record

    Person
      first as String: String()
      last <- String()
      profession <- String()

      default Person()

      Person()
        ->
          first as String
          last as String
          profession as String
        //Keeps a reference to the caller's String
        this.first: first
        //The copy constructor: the record gets its own String
        this.last: String(last)
        //The copy operator: copies the value into the record's own String
        this.profession :=: profession

      override operator ? as pure
        <- rtn as Boolean: first? and last? and profession?

      operator $ as pure
        <- rtn as String: `${first}, ${last}, ${profession}`

  defines program

    SharedOrCopied()
      stdout <- Stdout()

      firstName <- "John"
      lastName <- "Smith"
      jobTitle <- "IT"
      smithj <- Person(firstName, lastName, jobTitle)
      stdout.println(smithj)

      //The caller changes its own Strings in place
      firstName += " Jr"
      lastName += " Jr"
      jobTitle += " Manager"
      stdout.println(`${firstName}, ${lastName}, ${jobTitle}`)

      //Only 'first' changed: it is the caller's String
      stdout.println(smithj)
John, Smith, IT
John Jr, Smith Jr, IT Manager
John Jr, Smith, IT

A caller that changes such a variable and never reads it again is rejected (E08090), even though the change reaches the record:

Fails with E08090: is not referenced anywhere, or not referenced after assignment

#!ek9
defines module bad.site.structure.personaliasunread

  defines record

    Person
      first as String: String()

      default Person()

      Person()
        -> first as String
        this.first: first

      override operator ? as pure
        <- rtn as Boolean: first?

      operator $ as pure
        <- rtn as String: first

  defines program

    ChangedButNeverRead()
      stdout <- Stdout()
      firstName <- "John"
      smithj <- Person(firstName)
      firstName += " Jr"
      stdout.println(smithj)

Every argument, and every '<-' or ':' of an existing object, shares it in the same way; the copy principle says what ':=:' copies for each kind of type.

Why EK9 does it this way. In our experience, declaring a variable and inferring its type with <- leads to less reassignment of existing variables and better variable names. EK9 values that clarity of intention above memory use.

Class

defines class

A class also holds structured data (an aggregate), but you can add methods and control their visibility (public, protected and private). Unlike a record's, the fields of a class are always private; see basics on visibility. There are no protected fields and no secret, privileged or friend access to them. That applies information hiding and the principle of least knowledge, and you never choose a field's visibility: it is private.

The example below has abstract classes, classes that can be extended and classes that cannot. As in Kotlin, a class cannot be extended unless it is marked open or abstract, and an overriding method must say override.

Why EK9 does it this way. Some developers see closing classes by default as a bad approach. It is there to push designs towards traits and composition rather than inheritance alone, and EK9 has specific syntax for composition.

#!ek9
defines module site.structure.classes

  defines class

    //If you say it is abstract then it is by default 'open' so it can be extended
    Base as abstract
      singleMethod() as abstract
        <- rtn as String?

    //This cannot be extended as it is not 'abstract' nor 'open'
    Extension1 extends Base

      //declaration and initialisation of a field/property
      aField as Boolean: true
      //Inferred type supported for simple unset declarations
      bField <- String()

      updateAdditionalFields()
        -> additional as String
        bField :=: additional

      otherMethod()
        <- rtn String: $ aField

      //must provide this method as Base has declared it but not implemented it.  
      override singleMethod()
        <- rtn String: "Steve"

      default operator ?

    //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"

Methods declare return values in the same way. The example uses ':', '=' and ':=' to show what is possible; in practice, pick one style for a context and keep to it. You might always use ':' for return values, for example: it is terse. The choice is yours.

Any non-private method can be overridden in a subclass, but the override must say override.

Classes and methods cover extends, abstract and open in detail.

A source file can hold as many classes as you like. Grouping small, related classes in one file makes development easier; a file of many large or unrelated classes makes it harder. Once a file grows past about 500-1000 lines, split it; the classes can stay in the same module.

EK9 does not make a source file take its construct's name, which makes navigation harder. Agree reasonable working practices and standards with your team, and then argue about them (as is our way).

A Non Aggregate Construct

Functions are first-class constructs in EK9 and come in several varieties. They are not aggregates, but some kinds of function can hold state in variables that live beyond their invocation (a little like a closure).

Function

defines function

A function performs an operation, with optional parameters passed in and out. It is like a function in C, with some important differences:

#!ek9
defines module site.structure.filter
  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
      acceptable as stringFilter := nonColon
      if acceptable(toTest)
        stdout.println(toTest + " does not have a colon")

      //As a delegate we can swap the function out for another compatible one.
      acceptable = isSet
      if not acceptable(toTest)
        stdout.println("The value toTest is not set")

The example defines an abstract function signature and concrete functions that match it. You need the abstract function only to treat functions polymorphically. The program uses the function in two ways.

Functions and dynamic functions have much more detail. Dynamic functions suit short functions like these, and are similar to a lambda.

Parameters in and out are declared with -> and <-, as in the examples above. When calling a function (or a method on a class), the arguments go inside ().

Importantly, all parameters are passed by reference: EK9 has no primitives. To keep a value passed in, copy it. This matters most when initialising aggregates.

The example calls the function directly and as a delegate. Because both concrete functions extend the abstract function, functions can be used in an object-oriented way. The variable acceptable is of type stringFilter, so it can hold any compatible function: nonColon or isSet, which both extend stringFilter. It is declared with ':='; ':' or '=' would do as well.

This also gives you higher order functions; see functions. A function used as an object, and called through a delegate, can do much more than it first appears.

Why EK9 does it this way. Classes and functions are equals in EK9, and each has influenced the other as the language evolved. Dynamic functions and dynamic classes go further, and combined with composition they give more ways to solve problems and reuse code. Strong typing and abstract functions join the simple function to an object-oriented idea: type-safe function polymorphism. Pipeline processing, later, is built on it.

Trait

defines trait

A trait is a little like a Java interface with default methods, but closer to a Scala trait. Classes exhibit traits, or characteristics: a Bird class might have the trait 'Winged', and a Penguin the trait Swimmer but not Fly.

The name Trait says that a class shows some behaviour without necessarily having an 'IS A' relationship.

EK9 adds ways for a trait to restrict and control the classes that use it, so a trait is more than a set of method signatures.

This longer example uses shapes to show how traits work, including things few other languages support. A trait is like a class in many ways: its methods can be abstract (signatures with no body) or have full implementations. A trait can never be instantiated itself, unlike a class; but if it implements all its methods, you can create a dynamic class from it without adding any. Traits can also extend other traits.

defines trait

  ShapeLimit allow only Ellipse, Circle, Rectangle, Square, Triangle
    printDrawing()
      -> message as String
      //method body
      stdout <- Stdout()
      stdout.println(message)

The trait ShapeLimit has two purposes:

This statement does it:

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

The named classes are defined later in the example. The abstract class Shape is not named: only concrete classes may be listed (E05250), and an abstract class may take the trait without being listed. If you don't want to limit a trait's use, leave out allow only and the list.

There is a cost. Normally the classes that use the trait 'ShapeLimit' depend on it, but it does not depend on them. Listing them with allow only makes the dependency mutual, so be sure it is worth it.

The example continues with an abstract class Shape and 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 extends Ellipse
    override draw()
      printDrawing("Circle")

  Rectangle extends Shape
    override draw()
      printDrawing("Rectangle")

  Triangle extends Shape
    override draw()
      printDrawing("Triangle")

  Square extends Shape
    override draw()
      printDrawing("Square")

How the ShapeLimit trait is used:

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

This declares a class that is open to extension, because it is abstract. Being abstract, Shape cannot be instantiated (with Shape()). Its new abstract method draw() must be implemented by every non-abstract subclass. If object-oriented development is new to you, look at Wikipedia or other resources first.

as is optional syntax that sometimes makes code easier to read. Likewise, extends and is mean the same; use whichever reads better.

A class must be marked open to be extended; by default it is not:

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

The full example is a hierarchy of classes controlled by a trait. The trait provides the method printDrawing, and constrains and limits which traits and classes may use it. Had it declared an abstract method, every concrete class would have to implement it.

Why EK9 does it this way. The trait grew from a minimal 'protocol' construct into one with real control of its own. Through abstraction it is the 'glue' that links and controls classes, and with composition it does even more. Don't think of a trait as inferior to a class, or as little more than an abstract class.

More advanced features/constructs

Depending on the languages you know, what you have seen so far may look like ideas from other languages with a different syntax. To some extent it is, with a little added capability.

If these ideas are new to you, spend time using what you have learned before going on; the next sections are more advanced.

Generics/Templated types

Generics, or parametric polymorphism as it is more formally known, apply to both functions and classes in EK9.

Generics (Functions)

Generic programming defines functionality independent of type.

Generic functions use dynamic functions, which are introduced briefly here.

#!ek9
defines module site.structure.genericfunction

  defines function

    compareFunction() of type T as open
      ->
        item1 as T
        item2 as T
      <-
        rtn as Integer: item1 <=> item2  

  defines program

    ComparatorDemo()
      stdout <- Stdout()
      intComparator <- () is compareFunction of Integer as function
      stringComparator <- () extends compareFunction of String as function

      intResult <- intComparator(2, 8)
      stringResult <- stringComparator("two", "eight")

      stdout.println(`Result of int comparison is ${intResult}`)
      stdout.println(`Result of string comparison is ${stringResult}`)

Running the program 'ComparatorDemo' prints:

Result of int comparison is -1
Result of string comparison is 15

Some of this syntax is new:

compareFunction() of type T as open

This function uses a type called T. T stands for a type that is named later; it is not a real type such as String, and it could be any type at all. Any name works, such as S, K or V; the convention is a single upper-case letter. The function is also declared as open: the dynamic functions in the program extend it, and a function, like a class, cannot be extended unless it says so (E05030).

With two incoming parameters, -> goes on a line of its own and the parameters are indented below it. That is how you declare several incoming parameters; see parameter passing.

item1 as T
item2 as T

In the body, all the standard operators are assumed on the notional type T. They are checked only when you use the generic function with a specific type.

The function compareFunction returns the result of the comparison (spaceship) operator '<=>':

rtn as Integer: item1 <=> item2

EK9 does not use < > for generics, as C++ and Java do. '<=>' compares two objects, and you can override it in your own records and classes.

The program uses the generic function twice:

intComparator <- () is compareFunction of Integer as function
stringComparator <- () extends compareFunction of String as function

This is one form of dynamic function; other forms apply to non-generic functions. The lines take the generic function compareFunction and use it with an Integer and with a String.

In effect, the compiler takes the generic template, replaces T with Integer or String, and checks that every operator it uses exists on that type.

intResult <- intComparator(2, 8)
stringResult <- stringComparator("two", "eight")

The two new functions can be called and are strongly typed: one accepts only Integers, the other only Strings.

Generics/templates shows how to constrain T to particular types, and has more on generic functions. Generic classes, below, follow the same idea.

Generics (Classes)

This example is a finite state machine. It is much longer, and shows generic functions and classes used together, with a dynamic function whose body is overridden.

You can refer to FSM before it is defined. EK9 compiles in several passes, so this works.

#!ek9
defines module site.structure.fsm

  defines type

    FSM of Integer

  defines function

    FSMListener() of type T as abstract
      ->
        fromState as T
        toState as T

  defines class

    FSM of type T
      currentState as T: T()
      listener as FSMListener of T?

      FSM()
        -> initialState as T
        require initialState?
        currentState: initialState
        listener: () is FSMListener of T as function
          require fromState? and toState?

      default private FSM()

      setState()
        -> newState as T
        require newState?
        if listener?
          listener(currentState, newState)
        currentState: newState

      getState()
        <- rtn as T: currentState

      setListener()
        -> listenerToAdd as FSMListener of T
        require listenerToAdd?
        listener: listenerToAdd

      default operator ?

  defines program

    Driver()
      myListener <- () is FSMListener of Integer as function
        stdout <- Stdout()
        require fromState? and toState?
        stdout.println(`Changing from ${fromState} to ${toState}`)

      myMachine <- FSM(2)
      myMachine.setListener(myListener)
      myMachine.setState(5)

The new syntax:

defines type

As well as enumerations (before), a type section can forward-declare a generic class used with a specific type:

FSM of Integer

The generic function FSMListener works as in the previous section. New here is the generic class FSM of type T, with two (private) fields:

currentState as T: T()
listener as FSMListener of T?

Its methods also work with the notional type T. The Driver program uses a dynamic function as a listener. It gives the type (Integer) and also overrides the body of the generic function FSMListener: it checks the states are valid and prints the change from one state to the next.

myListener <- () is FSMListener of Integer as function
  stdout <- Stdout()
  require fromState? and toState?
  stdout.println(`Changing from ${fromState} to ${toState}`)

The incoming parameters fromState and toState need not be declared again; the overriding body uses them.

So a generic function is defined, used with a type, and then overridden as if it were a class.

Next, the generic class is used with type inference:

myMachine <- FSM(2)

Because 'FSM of Integer' was forward-declared, EK9 works out from the Integer constructor argument that 'myMachine' is an 'FSM of Integer'. The listener is then set, and setting the state to '5' triggers it.

Generics/templates has more on generic programming.

A generic state machine with listeners is very reusable: there can be any number of listeners, for a range of types, and each dynamic function supplies the behaviour you want.

Component

defines component

A component is much like a class. Its fields are private, but its methods can only be private or public (E07250). A component models a significant concept, often a singleton, in a solution, and it can be injected into other constructs such as classes and functions. For injection you define an abstract base component and extend it with concrete implementations.

Components limit coupling between modules and subsystems: they are a coarser building block, so implementation details stay inside. Don't let the low-level detail of a component 'bleed out'; that is the main reason all exceptions are unchecked in EK9. Hide as much of your implementation behind components as you can: it makes it easier to plug in new implementations, refactor and isolate your code.

The example below has three implementations of a 'Base Configuration'. You might use one during development or testing and another in production, or move from one store to another in the next release of the application. Either way, every class and function needs to know what the store is called.

You might also keep a range of implementations and choose the major components only once the final solution is decided. This suits companies that build core sets of components and then agree with each client which existing or new components to use.

defines component

  BaseConfiguration as abstract
    getFileStoreName() as abstract
      <- rtn as String?

  MainStoreSolution extends BaseConfiguration
    override getFileStoreName()
      <- rtn as String: "MainStore"

  SecondaryStoreSolution extends BaseConfiguration
    override getFileStoreName()
      <- rtn as String: "SecondaryStore"

  DefaultStoreSolution extends BaseConfiguration
    override getFileStoreName()
      <- rtn as String: "DefaultStore"

A real component would have more methods (load, save and so on), and an application is usually made of many components: here a 'Base Configuration' for storage, but there could also be 'Service Connectors' and links to other systems. The right 'Base Configuration' still has to be selected and 'wired in'; application below shows how.

The components section has a more detailed example.

Application

An application is a chosen configuration of one or more components. Build a library of classes, functions and traits that could be used, compose some of them into components, and compose an application from those components. The application is added to the library too, as a major item.

In EK9 a component is a fairly large 'lump' of a system that uses several classes, functions or other components. CustomerStorage, for example, could handle all customer information; wrapped as a component, it can later be replaced by another storage solution.

The code below has three applications. A real one registers many components for different parts of its work: AppV1 and AppV2 might use 'Redis' for caching and AppV3 'Memcached'.

The idea is not to build very large monolithic applications. Design note: from components to microservices explains why, and when to move to microservices.

defines application

  AppV1
    register MainStoreSolution() as BaseConfiguration
    register RedisCache() as DataCache

  AppV2
    register SecondaryStoreSolution() as BaseConfiguration
    register RedisCache() as DataCache

  AppV3
    register DefaultStoreSolution() as BaseConfiguration
    register MemCached() as DataCache

Think of car manufacturing. Small parts (bearings, gears, valves) are combined into subsystems (an engine, a gearbox); subsystems are chosen for each model of car; the cars go to the distributor to be sold.

Only when a car is sold does the owner drive it away. In EK9 that is the program construct, with one more piece of syntax that chooses which application to use (like choosing which car to buy).

Why have 'MainStoreSolution', 'SecondaryStoreSolution' and 'DefaultStoreSolution'? A real case: you migrate from the existing implementation ('MainStoreSolution') to a higher-performance one ('SecondaryStoreSolution'), and keep 'DefaultStoreSolution' to hedge your bets, in case 'SecondaryStoreSolution' does not work out or because it costs less to run.

Why EK9 does it this way. If you are new to development, this may look contrived. With experience you know that in large, long-lived software, preparing for this from the outset is essential, and the larger the software the more it matters. It is a main reason to use components once software passes a certain size; adopt them neither too soon nor too late.

defines class

  ConfigHandler
    //Because BaseConfiguration is an abstract component it will get injected
    //Note the use of the ! suffix
    config as BaseConfiguration!

    showConfigDetails()
      stdout <- Stdout()
      stdout.println(config.getFileStoreName())

    default operator ?

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

If the code is in a file called configtest.ek9, run each program like this:

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

The user chooses 'v1Program', 'v2Program' or 'v3Program', each with its application. So where is the advantage?

Suppose the 'Base Configuration' is used in many classes and functions, not only the class ConfigHandler. To swap it, you would normally find every reference to 'MainStoreSolution' and change it to 'DefaultStoreSolution' (for example), modifying all that code.

This is where Inversion of Control comes in: 'v3Program', with the configuration of 'AppV3', decides that the 'DefaultStoreSolution' component is used. Every class and function still needs the chosen configuration, and that is Dependency Injection: by declaring the component 'config BaseConfiguration', ConfigHandler asks for it to be injected. It does not know which implementation it will get; it has given away control.

Aspect Oriented Programming

This snippet is an early view of AOP. Registering components in an application is also how you define the aspects that intercept calls.

register Solution1() as BaseConfiguration with aspect of TimerAspect(SystemClock()), LoggingAspect("DEBUG")

This wraps the calls on the methods of the component 'MainStoreSolution' with AOP calls on the classes TimerAspect and LoggingAspect (not shown in detail). Every time getFileStoreName(), or any other method on that component, is called, the call is intercepted and beforeAdvice() and afterAdvice() are called on the configured aspects.

Aspects suit Cross Cutting Concerns. To log every call on a component, or time how long it takes, leave the component as is and use AOP to intercept the calls.

Typical uses:

The component then holds no logging or other extra code, only the functionality it needs, and the logging code, written once as an aspect class, is reusable across all components. That is Separation of Concerns and Reuse, and so more maintainable code.

Why EK9 does it this way. Components, applications and aspects are for larger software; small projects rarely need them, but at any reasonable scale they become essential. Only use them when you need to, in the right context. EK9 is meant to go from very small programs to very large solutions, and it is hard to build reliable, reusable and robust software at scale without these design patterns. That is why they are built into the language.

To wrap logging or transaction boundaries, or to time a web service from call to response, look to components and aspects. Write the logging code once and reuse it on any component.

register SimpleDevelopedSecurityProvider() as SecurityProvider

With DI/IOC you can define any number of abstract security privileges for the application without tying them to a real, concrete provider. Software can then be developed and tested without real calls to LDAP (for example) in unit and automated tests, and different SecurityProvider implementations let you test a range of error conditions.

Text

defines text

The examples so far print fixed text in quotes. Many applications must support several spoken languages and let users choose one (a locale). In the example below, localised text is bound into a 'text' object called ProductsText. Text objects are like classes focused on text, and support string interpolation through methods that take parameters.

Text objects are compiled; they are more than a set of property files. The parameters, and the interpolation that uses them, are checked.

That matters in a large application with a lot of user-facing text: to support several spoken languages, you need to manage and control its Strings. The text construct is designed for that.

#!ek9
defines module site.structure.text

  defines record

    Product
      name <- String()

      Product()
        -> productName as String
        name: productName

      default operator ?

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

      specialProduct <- Product("Special")
      stdout.println(text.productsTitle())
      stdout.println(text.moreDetailsOn(specialProduct))

The statement below is from the example. A text method only ever returns a String, so it declares no return value. Incoming parameters are optional (a method can be pure text), and so are the (), as when defining functions and methods.

moreDetailsOn()
  -> product as Product
  `More details on ${product.name}`

The parameter product can be any type; here it is Product, a record, whose public field name is read directly. '${' starts an interpolation and '}' ends it. An interpolated String starts and ends with a back tick `, and the whole String is returned once the interpolation is complete.

Package

defines package

The package construct lets you use other packages as dependencies, decide which source code to include in the compile, and give your software a version number. Packaging has the details.

Why EK9 does it this way. Many languages manage packaging and versions outside the language. EK9 puts it in the language itself: there are no external XML, JSON or YAML files to configure, only the language you are writing code in.

Finally edge services

The constructs so far are normal programming language concepts or design patterns (DI/IOC, aspects and so on). The last construct brings web-based development into the language itself.

Once components have made an application large enough, you need microservices (see the applications section). The service is how you create and use them.

Service

defines service

The introduction outlined web services; there is more here, and the full detail is in Web Services. A service makes applications/components available as HTTP/REST/RESTful web services. It uses object-oriented mechanisms where they apply, to keep new terminology to a minimum. Some parts of web services cannot, and must not, be hidden; EK9 tries to find a balance.

#!ek9
defines module site.structure.services

  defines component

    BusinessProcess as abstract
      process() as abstract
        -> detail as String
        <- rtn as String?

    StandardBusinessProcess extends BusinessProcess
      override process()
        -> detail as String
        <- rtn as String: `{"processed": "${detail}"}`

  defines function

    //Every operation below answers through this one response, to keep the example short.
    jsonResponse() as pure
      -> json as String
      <- response as HTTPResponse: (json) with trait of HTTPResponse
        override content()
          <- rtn as Bytes: Bytes().withUtf8(json)
        override status() as pure
          <- rtn as Integer: 200
        override contentType() as pure
          <- rtn as String: "application/json"
        override cacheControl() as pure
          <- rtn as String: "no-cache"
        override contentLanguage() as pure
          <- rtn as String: "en"
        default operator ?

  defines service

    Addresses for :/addresses open

      //A GET (the default verb for a named operation)
      byId() for :/{address-id}
        ->
          addressId as String :=: PATH "address-id" //required because different name
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: jsonResponse(`{"id": "${addressId}", "language": "${acceptLanguage}"}`)

      listByPostCode() for :/{postCode}/
        ->
          postCode as String //Now assume PATH PARAM because there is a param
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: jsonResponse(`{"postCode": "${postCode}", "language": "${acceptLanguage}"}`)

      list() for :/
        ->
          postCode as String :=: QUERY "postCode"
          acceptLanguage as String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: jsonResponse(`{"query": "${postCode}", "language": "${acceptLanguage}"}`)

      //A POST adds to the collection
      operator += for :/
        -> request as HTTPRequest :=: REQUEST
        <-
          response as HTTPResponse: jsonResponse(request.content().toText("utf-8"))

      //A DELETE removes from it
      operator -= for :/{addressId}
        -> addressId as String
        <-
          response as HTTPResponse: jsonResponse(`{"deleted": "${addressId}"}`)

      //A PUT replaces a resource
      operator :^: for :/{addressId}
        ->
          addressId as String //assume PATH
          content as String :=: CONTENT
        <-
          response as HTTPResponse: jsonResponse(`{"replaced": "${addressId}", "with": ${content}}`)

      //merge is a PATCH
      operator :~: for :/{addressId}
        ->
          addressId as String //assume PATH
          content as String :=: CONTENT
        <-
          response as HTTPResponse: jsonResponse(`{"merged": "${addressId}", "with": ${content}}`)

    //Here is customer service (only partial)
    //But shows verbs 'GET', 'PUT' etc named explicitly
    Customers for :/customers open

      //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 String :=: HEADER "Accept-Language"
        <-
          response as HTTPResponse: jsonResponse(`{"customer": "${customerId}", "language": "${acceptLanguage}"}`)

      replace() as PUT for :/{customerId}
        ->
          customerId as String :=: PATH
          content as String :=: CONTENT
        <-
          response as HTTPResponse: (customerId, content) with trait of HTTPResponse
            businessProcess as BusinessProcess! // this gets injected as a component
            //Now use the business process to build the response.
            override content()
              <- rtn as Bytes: Bytes().withUtf8(businessProcess.process(`${customerId} ${content}`))
            override status() as pure
              <- rtn as Integer: 200
            override contentType() as pure
              <- rtn as String: "application/json"
            override cacheControl() as pure
              <- rtn as String: "no-store"
            override contentLanguage() as pure
              <- rtn as String: "en"
            default operator ?

  //Application that exposes the web services to be made available.
  defines application

    CustomerAccess
      //Register the business component
      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()
      stdout.println("Now starting REST Services")

      //Serve on socket port 9092, until the server is stopped.
      try
        -> server <- HTTP(NetworkProperties(9092))
        server.serve()

This maps a service to a URL:

Addresses for :/addresses open

A URL such as https://www.somehost.com/addresses is then served by Addresses. Caching, how long a response may be kept before it is requested again, is key to web services. A response is sent back by implementing the HTTPResponse trait. It has more methods than content() and status, including these for caching:

It has other methods, but these are the key to effective HTTP caching.

A service should do very little processing itself, and delegate most of the work to a component; below, a 'businessProcess' is injected by DI/IOC. The service layer fills the HTTPResponse with enough data and concentrates on supporting caching. It should also read the HTTP headers, which are full of hints and options such as Accept-Language, and use them to give the best response it can.

businessProcess as BusinessProcess! // this gets injected as a component

This HTTP/REST method retrieves an address with a GET:

byId() for :/{address-id}
  ->
    addressId as String :=: PATH "address-id" //required because different name

The parameter address-id is not a valid EK9 variable name, and HTTP parameter names in general are not. So the HTTP parameter is copied (:=:) into a valid EK9 method parameter. It can be copied from PATH, HEADER, QUERY, CONTENT or REQUEST.

-> request as HTTPRequest :=: REQUEST

Web services map HTTP parameters into incoming method parameters, converting incoming "String" values into strong types. Defining a service only adds it to a library of functionality; it takes effect when an application registers it. A program then starts and runs the application, CustomerAccess in this case.

Why EK9 does it this way. EK9 does not mask or hide the HTTP protocol. It maps it onto mechanisms like EK9's component/class constructs, and reuses the component and application constructs to deploy the services. The aim is to get the data or process that REST triggers into strong types as soon as possible, and let components do the processing. The construct is an adapter, focused on caching and on different representations in responses.

REST/RESTful Summary

Once you move from a single executable application to microservices, you need to have embraced 'devops': you are now a 'network engineer' too. Use HTTP caching, containers, rigorous build pipelines and automated rolling deployments; ideally you will already do some of this for your monolithic application. Keeping public interfaces compatible, and rolling out new software that breaks that compatibility, takes coordination and planning. Web services discusses this further.

Summary

This page has shown every construct EK9 offers. To recap:

EK9 is a big language, but you can use only the parts you need.

Next Steps

For the language in fine detail, rather than its structure, read basics next: variables and control statements. You can also use the navigation bar to jump around.