EK9 in Depth

Where Volume 1 Left You

Getting Started with EK9 taught the language to a reader who had never programmed. This volume does not repeat it. If you read that book, this chapter is the ledger of what it promised and where each promise is kept, and you can skim to the map at the end. If you did not, because you already write Java, Kotlin, C#, Rust, Go or Python, this chapter is the primer: ten minutes, one program, and the places a fluent programmer trips first. Every listing here was compiled and run.

The syntax in one table

EK9 is not C, Java, C# or Python, and the differences are mostly subtractions. This is the compiler's own summary of them (ek9 -h -E4), reworded:

You would write In EK9
{ } and ; Indentation (two spaces) is structure; a newline ends a statement; if x > 5, switch x, case y are bare
return You name the result in the signature, <- rtn as Type, and assign it: rtn: value. The compiler proves every path assigns it
break, continue, goto, fall-through Stream pipelines, guards, and several values in one case
null None. A value is absent, unset or set; ? asks "set?"
new, var, let, final, static, void, public, private, interface Person() builds; <- declares; fields are always private; defines constant at module level; trait
&&, ||, ! and, or, not
Type[], Type<T> List of Type, Type of T
int, string, bool, float Integer, String, Boolean, Float: every type is an object, there are no primitives
package, import defines module x.y and a references block
Safe navigation (Kotlin's ?.) A guard: if x <- maybe()
extends X implements Y extends X with trait of Y

Three operators do the work of declaration and assignment. <- declares a new variable. := assigns an existing one. :=? assigns only if the target is still unset. Strings with values inside use backticks, `Hello ${name}`; in double quotes ${name} is literal text. Two keyword synonyms exist for readers who prefer them: given for switch and when for if.

One program

Here is a stock check: a record, a constant, three small pure functions, a stream pipeline and a guard. Read it once for its shape.

#!ek9
defines module book.primer

  defines constant
    REORDER_LEVEL <- 5

  defines record
    Item
      name <- String()
      stock <- Integer()

      default Item() as pure

      Item() as pure
        ->
          itemName as String
          itemStock as Integer

        name :=? String(itemName)
        stock :=? Integer(itemStock)

      operator $ as pure
        <- rtn as String: `${name} (${stock})`

      default operator ?

  defines function
    needsOrder() as pure
      -> item as Item
      <- rtn <- Boolean()
      rtn: item.stock < REORDER_LEVEL

    describe() as pure
      -> item as Item
      <- rtn <- String()
      rtn: $item

    find() as pure
      ->
        wanted as String
        items as List of Item
      <- rtn <- Optional() of Item

      for item in items
        if item.name == wanted
          rtn: Optional(item)

  defines program
    Tally()
      stdout <- Stdout()
      items <- [Item("pen", 12), Item("ink", 3), Item("pad", 4)]

      low <- cat items | filter by needsOrder | map with describe | collect as List of String
      stdout.println(`Reorder: ${low}`)

      if ink <- find("ink", items)
        stdout.println(`Found ${ink.get()}`)
      else
        stdout.println("No ink")

      stdout.println(`Found staples? ${find("staples", items)?}`)

It prints:

Reorder: ["ink (3)", "pad (4)"]
Found ink (3)
Found staples? false

Five things in it are not what you would guess.

Construct names are the keywords. defines record, defines function, defines program and defines constant each open a block of that construct. A record carries data and operators and holds no methods; a class hides its data behind methods; a trait is a bundle of behaviour; a component hides an implementation behind an interface. A function is a first-class, single-purpose callable, not a method of something. The same constructs recur in every chapter.

default operator ? is mandatory. An aggregate with fields must say what "set" means for it. default asks the compiler to write the obvious one from the fields; without it, the record does not compile (E07235). EK9 has a fixed list of operators, each with a fixed meaning, and default operator writes the mechanical ones (<=>, $, $$, #?, :=:, ?) so that you do not.

pure is enforced. A pure function may not reassign anything but its own return, may not mutate what it was handed, and may call only other pure code. It is not "no side effects" - a pure function may print - and there is no escape hatch. Operators and Pragmatic Purity, in Full is the chapter on what that costs and buys.

The stream line replaces a loop.

low <- cat items | filter by needsOrder | map with describe | collect as List of String

cat names the source, each | is a stage that takes a function by name, and collect as ends it; a pipeline must end in a terminal. There is no break and no continue, so "stop early" and "skip this one" are filter, head and their kin.

The guard runs the block only when the value is set.

if ink <- find("ink", items)
  stdout.println(`Found ${ink.get()}`)
else
  stdout.println("No ink")

ink.get() is legal only because the check sits in the same block; take the if away and the compiler refuses the get(). Guards work identically in if, switch, for, while and try. That one pattern is the front line of EK9's answer to null, and Optional and Result, in Depth is the chapter on exactly how far it reaches.

The first things that trip a fluent programmer

These three cost more first attempts than anything else, because the compiler enforces as errors what other languages leave to a style guide.

A literal in a comparison must be named. The 5 below is an error, E11064; the same literal repeated three times is E11065. Zero, one and minus one are exempt.

if stock < 5
  rtn: true
EK9     : Error   : E11064: '5' on line 8 position 17: '5' literal in comparison/range - give it a meaningful name
             See: ek9 -h E11064

The fix is a defines constant, as REORDER_LEVEL above. It costs a line and it puts the intent in a name. The cost is that you cannot write a throwaway threshold inline; what it buys is that every number in a condition has a reason attached.

Four or more arguments must be named. lineTotal(9.99, 2, 0.1, 0.2) is E11062, and so is a call with two literal true/false arguments. Name them all, in declaration order, with the declared parameter names: lineTotal(price: 9.99, quantity: 2, discount: 0.1, taxRate: 0.2).

EK9     : Error   : E11062: '(' on line 17 position 40: call has 4+ positional arguments - use named arguments for clarity
             See: ek9 -h E11062

operator ? takes override on a plain class or record; the others do not. ? is inherited from the root type, so a hand-written one replaces it; $, <=>, == and #? take override only when a parent defines them. Get it wrong and the compiler tells you which way.

EK9     : Error   : E05120: '?' on line 8 position 15: 'override' required on 'public Boolean <- ?()' in 'Meter' (add it to THIS member only - a member with no parent version must not carry 'override', E05110) as it overrides method of same name/signature in hierarchy
             See: ek9 -h E05120

Behind all three is the same idea. EK9 does not leave quality to review: the compiler is the single gate, and "if it compiles, it is good" is a statement about that gate. Its limits are real and this book states them where they bite.

A note on field style

Volume 1 declared fields with the type spelled out, count as Integer: Integer(), because naming the type was part of learning it. This volume uses the terser count <- Integer(). Both compile and behave the same:

#!ek9
defines module book.habits

  defines constant
    REORDER_LEVEL <- 5

  defines record
    Spelled
      count as Integer: Integer()
      label as String: String()

      default operator ?

    Terse
      count <- Integer()
      label <- String()

      default operator ?

  defines function
    verdict() as pure
      -> stock as Integer
      <- rtn <- String()
      given stock
        case 0
          rtn: "none left"
        default
          when stock < REORDER_LEVEL
            rtn: "reorder"
          else
            rtn: "fine"

  defines program
    Habits()
      stdout <- Stdout()

      spelled <- Spelled()
      terse <- Terse()
      stdout.println(`spelled: ${spelled.count?} ${spelled.label?}`)
      stdout.println(`terse:   ${terse.count?} ${terse.label?}`)

      stdout.println(verdict(0))
      stdout.println(verdict(3))
      stdout.println(verdict(9))
spelled: false false
terse:   false false
none left
reorder
fine

The two records are the same to the compiler; the example also uses the given and when synonyms. When you meet the longer form in Volume 1, or in older code, read it as the shorter one.

What Volume 1 promised

Volume 1 pointed forward in several places. Each is kept in the second volume, and each chapter opens by quoting the sentence it pays.

Volume 1 said, in The promise Kept in
Making Decisions and Repeating Work Exceptions are for when something goes wrong; the second volume covers them Errors and Exceptions
Testing as You Go The second volume goes into testing, coverage and test-driven development in depth Testing the Compiler Trusts
Collections The second volume covers JSON in full A Web Service, Step by Step, and the JSON sink in Stream Pipelines in Depth
Optional and Result Unchecked access, a discarded result and a stale check are compile errors Optional and Result, in Depth
Streams and Pipelines The other pipeline verbs; a one-in, one-out function that is not pure is a Routine Stream Pipelines in Depth, Functions as Values: the Type Hierarchy
Records and Enumerations The second volume moves to the shorter field form the previous section
Abstract Types, Subtypes and Polymorphism The dispatcher; one function type, answered by many concrete functions The Dispatcher, Functions as Values: the Type Hierarchy
Traits and Composition Stacking traits is a topic for the second volume Dynamic Functions and Classes
Components Splitting a project across modules, and injecting a component Organising a Growing Codebase, Dependency Injection and Applications
Using Generics Writing your own generic types Write-Your-Own Generic Types and Functions
Using Operators Mutating and non-mutating operators, an exact, enforced model Operators and Pragmatic Purity, in Full
Build a Game, End to End You cannot unit-test a conversation; the second volume shows how to inject a program's surroundings Dependency Injection and Applications, Testing the Compiler Trusts
The Evidence The second volume returns to Hoare's null mistake and to Liskov substitution The Payoffs: Liskov and Hoare

The map

The volume has three movements. They are not Parts of the printed book; they are how its chapters hang together.

The first movement: why the safety holds

Optional and Result, in Depth takes the guard you used and shows what the compiler's flow analysis can see and what it refuses to. Operators and Pragmatic Purity, in Full gives the exact operator model and the purity rules. Functions as Values shows how every function gets a type from its shape. Dynamic Functions and Classes adds capture, Stream Pipelines in Depth uses it, and The Dispatcher is the object-oriented cousin of both. Write-Your-Own Generic Types and Functions is where you author what you used.

The second movement: building real systems

Organising a Growing Codebase (with an opinionated section on comments), Errors and Exceptions, Concurrency, Safely, Dependency Injection and Applications, Aspects, Networking: TCP and UDP, A Web Service, Step by Step as the capstone, and Packaging and Sharing Your Program.

The third movement: what it adds up to

Testing the Compiler Trusts, Quality Built In, and The Payoffs: Liskov and Hoare, which names the two problems the whole design answers.