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.