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.
- New to programming: programs and functions, then basics, operators and flow control. Records and classes can wait. Build up in stages; too many new ideas at once is overwhelming.
- Some experience: go as far as traits, then come back for generics and the rest.
- Experienced: work through them all. The patterns you already use (dependency injection, aspects, services) are built into the language. Mix the techniques to suit the software you write and the skills of your team.
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)
Download structure_semicolon.ek9
End each statement with a new line:
#!ek9 defines module site.structure.nosemicolon defines program Greeting() name <- "Steve" Stdout().println(name)
Download structure_no_semicolon.ek9
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
Full source: structure_constants.ek9 - Download structure_constants.ek9
<- (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
Download structure_constants.ek9
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
Full source: structure_person.ek9 - Download structure_person.ek9
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
Full source: structure_person.ek9 - Download structure_person.ek9
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
Full source: structure_person.ek9 - Download structure_person.ek9
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()
Full source: structure_declarations.ek9 - Download structure_declarations.ek9
- name1 holds no object yet. Declaring a variable this way, you must say explicitly that you have not initialised it (the ? suffix). The compiler does not let you read name1, even as name1?, until you have assigned it (E08020).
- name2 holds an object but no meaningful value, so name2? is false.
- name3 and name4 hold objects with values, so name3? and name4? are true. There is more on this in basics.
- name5? is true, but name6? is false.
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}`)
Full source: structure_person.ek9 - Download structure_person.ek9
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)
Download structure_person_named.ek9
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:
- The copy constructor:
this.first: String(first), as above. - The copy operator:
this.first :=: firstcopies the value into the field's own object.
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)
Download structure_person_alias.ek9
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)
Download structure_person_alias_unread.ek9
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)
Full source: structure_shapes.ek9 - Download structure_shapes.ek9
The trait ShapeLimit has two purposes:
- It provides a way to print a message to Stdout.
- More importantly, it limits which traits and classes may extend it, like Java's sealed classes.
This statement does it:
ShapeLimit allow only Ellipse, Circle, Rectangle, Square, Triangle
Full source: structure_shapes.ek9 - Download structure_shapes.ek9
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")
Full source: structure_shapes.ek9 - Download structure_shapes.ek9
How the ShapeLimit trait is used:
Shape with trait of ShapeLimit as abstract
Full source: structure_shapes.ek9 - Download structure_shapes.ek9
//Could have been written in shorthand like this Shape with trait ShapeLimit as abstract
Full source: structure_shapes_shorthand.ek9 - Download structure_shapes_shorthand.ek9
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
Full source: structure_shapes.ek9 - Download structure_shapes.ek9
//This could have been written like this Ellipse is Shape as open
Full source: structure_shapes_shorthand.ek9 - Download structure_shapes_shorthand.ek9
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}`)
Download structure_generic_function.ek9
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
Full source: structure_generic_function.ek9 - Download structure_generic_function.ek9
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
Full source: structure_generic_function.ek9 - Download structure_generic_function.ek9
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
Full source: structure_generic_function.ek9 - Download structure_generic_function.ek9
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
Full source: structure_generic_function.ek9 - Download structure_generic_function.ek9
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")
Full source: structure_generic_function.ek9 - Download structure_generic_function.ek9
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
Full source: structure_fsm.ek9 - Download structure_fsm.ek9
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?
Full source: structure_fsm.ek9 - Download structure_fsm.ek9
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}`)
Full source: structure_fsm.ek9 - Download structure_fsm.ek9
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)
Full source: structure_fsm.ek9 - Download structure_fsm.ek9
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"
Full source: structure_applications.ek9 - Download structure_applications.ek9
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
Full source: structure_applications.ek9 - Download structure_applications.ek9
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()
Full source: structure_applications.ek9 - Download structure_applications.ek9
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")
Full source: dependencyInjection_components_and_aspects.ek9 - Download dependencyInjection_components_and_aspects.ek9
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:
- Logging
- Transaction Management
- Timing/Performance
- Security - Limiting Access
- Caching
- Monitoring
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
Full source: structure_security.ek9 - Download structure_security.ek9
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}`
Full source: structure_text.ek9 - Download structure_text.ek9
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()
Download structure_services.ek9
This maps a service to a URL:
Addresses for :/addresses open
Full source: structure_services.ek9 - Download structure_services.ek9
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:
- age
- cacheControl
- date
- etag
- expires
- lastModified
- pragma
- refresh
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
Full source: structure_services.ek9 - Download structure_services.ek9
This HTTP/REST method retrieves an address with a GET:
byId() for :/{address-id} -> addressId as String :=: PATH "address-id" //required because different name
Full source: structure_services.ek9 - Download structure_services.ek9
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
Full source: structure_services.ek9 - Download structure_services.ek9
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:
- Constants - for fixed real world values
- Types - short and simple types or constraints on existing types
- Functions - the smallest unit of processing
- Records - very simple data aggregate, but with operators
- Classes - aggregate with data and a range of methods/operators, supporting inheritance/composition
- Traits - enables signatures to be established, facilitates composition and control
- Text - gathers spoken language text with string interpolation support
- Components - larger compositions of other constructs enabling encapsulation and injection
- Applications - defines the 'wiring' of a solution at a higher level
- Services - for RESTful, HTML and other HTTP based interactions
- Programs - the entry point to run your code
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.