EK9 Basics

This page covers layout, declarations, comments and visibility, and EK9's three-state object model. For what EK9 is, see the introduction; for the file layout and the constructs, see structure.

Getting Started

Symbols and Punctuation

EK9 marks blocks by indentation (see the introduction), not with { and }. Braces are used only to write a Dictionary (Map).

Line Termination

There is no ; (semicolon) anywhere, not even to end a line.

Angle brackets never enclose a generic type: a list of strings is List of String. As an enclosing pair, they appear only in comments.

There are no in-line lambdas as such, so -> and => are not used for them.

Accepting Parameters

-> marks the parameters a function or method accepts. <- marks what comes out: the returning value, or a constant or variable being declared, inferred and initialised (see declarations and type inference).

A call passes its arguments in ( and ), as in most languages. -> and <- appear only where a method or function accepts or returns them.

#!ek9
defines module site.basics.parameters

  defines function

    function1()
      -> n as Integer
      <- sum as Integer: n + n

    function2()
      ->
        n1 as Integer
        n2 as Integer
      <-
        sum as Integer: n1 + n2

There is zero or one named returning value; multiple return values are not supported. Every parameter is passed by reference, so a function can change an incoming parameter by 'copying' data into it, a little like an 'inout' parameter in some languages (see parameter access).

Why EK9 does it this way. Declaring parameters this way makes the text less dense, and it encourages fewer parameters.

Sanitized Parameters

The sanitized modifier on an incoming String parameter cleans user input on the way in, against injection attacks (SQL injection, XSS, command injection and others). It is part of EK9's "Security by Default" philosophy.

#!ek9
defines module site.basics.sanitized

  defines function

    executeQuery()
      -> sql as sanitized String
      <- result as String: `Executing: ${sql}`
      //The 'sql' parameter is automatically sanitized on entry,
      //before any of this function's code runs - preventing SQL injection

    processUserInput()
      ->
        name as sanitized String
        comment as sanitized String
      <- success as Boolean: name? and comment?
      //Both parameters are sanitized before use

How it works: a function or method with a sanitized parameter cleans it on entry, however it was called. It replaces the parameter with its own defensive copy of the String and sanitizes that copy before any of its code runs. So:

Restrictions:

Why direct assignment is blocked (hidden aliasing): after local <- sanitizedParam both names refer to the same sanitized copy, so a change through one shows through the other. That defeats the defensive copy and makes the code confusing.

Fails with E07930: cannot assign from sanitized parameter - creates hidden copy semantics

#!ek9
defines module bad.site.basics.sanitizedalias

  defines function

    //Creates a hidden alias
    processInput()
      -> input as sanitized String
      <- result as String: String()
      local <- input      // 'local' would be an alias to 'input'
      local += " modified" // would also modify 'input'!
      result: local

Make an explicit copy instead:

#!ek9
defines module site.basics.sanitizedcopy

  defines function

    //An explicit copy
    processInput()
      -> input as sanitized String
      <- result as String: String()
      local <- String(input)  // OK: Explicit copy constructor
      local += " modified"     // Only affects 'local'
      result: local

The same restriction covers operators that return one of their operands (the coalescing operators <?, >?, ?: and the ternary) and guards in control flow (if local <- input). See E07930 for the details and solutions.

The sanitization framework, including the InputSanitizer class and the enterprise Sanitizer components, is described in Input Sanitization. Related compiler errors: E07910, E07920, E07930, E07940.

Operators

EK9 uses symbols and symbol combinations for its operators, so that common ideas look the same everywhere: adding two values and adding an item into a collection both use the addition operators.

EK9 also enforces what an operator may do: some must be pure ($ and ==, for example; E07500), and the mutating ones change the object itself.

Why EK9 does it this way. Symbols use long-term memory for ideograms, which drives consistency. This is closer to C++ and Scala than to adding methods with inconsistent names and different meanings to each Object type. Take care when overriding operators: excessive, misleading or inappropriate use leads to code that is hard to understand and maintain.

Objects

EK9 has no primitive types: everything is an Object. Most languages have primitives, which are passed by value to methods and functions. In EK9 every value is passed by reference.

A method or function marked pure cannot modify what it is passed, even when that data is mutable.

In most languages a primitive int or float always has a value, even when declared without one (0 and 0.0f), because memory is always allocated to hold it. So a primitive can never cause a null pointer error? Not in a language with auto boxing (see here for what this means): converting between the Object form of a value and the primitive can still fail.

Auto-Boxing (not supported)
//If auto boxing was supported
//primitiveInt would be initialised as 0
primitiveInt as int
//objectInt would not have any memory allocated to hold its value
objectInt as Integer
//So what happens here?
primitiveInt = objectInt
//Some type of null exception will occur

Consistency

All the EK9 collection types work with any type of Object, and even functions can be passed as Objects (delegates). With no primitives there is no auto boxing, so that kind of null pointer error cannot occur.

Think in terms of Objects, not low-level primitives. There are no arrays; there are Lists and Dictionaries, and you read their contents with Iterators or with stream pipelines, as outlined in the introduction.

Why EK9 does it this way. In an object-oriented or functional model it is best to be consistent, and that is the main reason EK9 has no primitives. The cost is that Objects are much heavier than primitives a CPU can hold in a register, so the code generator has to work much harder to produce efficient instructions. For the developer, there is one model.

Tri-State Semantics: EK9's Object Model

Coming from Java, Python, or C++? There, a reference is null or not null. In EK9 every object is in one of three states, and the compiler understands and enforces them.

The Three States

State Meaning Example
Absent No memory allocated to hold a value - what other languages call null. EK9 is designed so that a program can never observe it None you can write: there is no null (E01073), and reading a variable that might not have a value is a compile error. A key not found by getOrDefault gives the default - typically an unset object - not an absent one
Unset The object exists but has no meaningful value yet name <- String()
a String object created with no value → unset
Set The object exists and holds a valid, usable value name <- "Steve"
has data → set

What "set" means depends on the type

This is where EK9 differs most from other languages, and new EK9 developers often find it confusing at first.

Type Category When is it "Set"? Rationale
Simple values
(String, Integer, Boolean, Float)
Only once it has been given a value
name <- String() → unset
name <- "Steve" → set
name <- "" → set
An object created with no value has no meaningful information to convey; an empty text is a value
Collections
(List, Dict, Set)
Always set when created, even if empty
items <- List() → set (0 items)
An empty collection is a valid state - "no items" is meaningful information
Containers
(Optional, Result, DictEntry)
Depends on what they hold An Optional is set only when the value it holds is set: Optional(String()) is unset, Optional("x") is set

Common Mistakes Table

❌ Wrong Assumption ✓ Correct Understanding
"Empty List is unset" Empty List is SET
List()? is true
"A new String is set" String() is UNSET
String()? is false (the literal "" is set)
"Dict with 0 items is unset" Empty Dict is SET
Dict()? is true
"Only unSet() makes things unset" Invalid construction also creates unset
Integer("invalid") is unset

Why This Matters: Set, Unset, and No Null

In EK9 the question is not "is it null?" but "does it have a meaningful value?":

Absent means no memory has been allocated to hold the value at all - what other languages call null. EK9 is designed so that a program can never observe it: the compiler rejects any read of a variable that might not yet have been given a value, and a guard (if user <- ...) only enters its body when the value is set.

This lets the compiler catch bugs that Java and Python cannot detect:

Example: Database Lookup

Java (2 states, ambiguous) EK9 (3 states, precise)
user = database.get(id);
if (user == null) {
  // Did lookup fail?
  // Or was user never looked up?
  // Or was key not found?
  // Can't tell!
}
if user <- database.getOrDefault(id, User())
  // user is SET: the lookup succeeded, the key was found
  rtn: `Found ${user.name}`
else
  // user is UNSET (the default was used): the key was not in the database
  // the guard ensures user is never used unset
  rtn: `No user with id ${id}`

The ? (is set) Operator

Every EK9 type has the ? (is set) operator. It answers one question: "Does this object hold a meaningful, usable value?" Built-in types define it. A record or class with fields must declare it, by writing it or with default operator ? (E07235).

What counts as meaningful depends on the type:

name <- String()
stdout.println(`name set: ${name?}`)    // false - unset

name: "Steve"
stdout.println(`name set: ${name?}`)    // true - set

items <- List() of String
stdout.println(`items set: ${items?}`)  // true - set (empty is valid for collections)

items += "first"
stdout.println(`items set: ${items?}`)  // true - still set (now has 1 item)

The full source also runs the guard above; it prints:

Found Steve
No user with id 2
name set: false
name set: true
items set: true
items set: true

JSON Integration Implications

The states matter when you process JSON. Reading a property with the JSON type's get:

JSON received The property json.get("age")
{"name": "Steve"} not provided unset
{"name": "Steve", "age": null} provided as null set, a JSON null
{"name": "Steve", "age": 42} has a value set, 42

So EK9 can tell "field not provided" from "field provided as null" from "field has a value", which matters for PATCH operations in REST APIs. Going the other way, $$ writes an unset property as null: {"name":"Steve","age":null}.

Getting Started with Tri-State

What to expect

Why EK9 does it this way. Tri-state is about semantic precision. It makes you think about what "no value" means in your domain - missing data? invalid input? a failed lookup? - and gives you the tools to express and enforce the difference.

See Also

Declarations

In EK9 a declared variable or constant is normally given memory, so it always "points somewhere". It can still be declared with no meaningful value.

Why declare something with no meaningful value? See the example below.

Variable declarations
//Declare an Integer - but with no value yet known.
age <- Integer()
//Declare an Integer - but with a known value from the outset.
minimumAge <- 21

age is an Integer with no value yet. If the program is about to ask the user for their age, it does not know it at this point. Other languages would use a marker such as -1, or leave an Object null. In EK9 age has memory allocated but is noted as having no meaningful value. minimumAge is known from the outset, and its type is inferred as Integer.

You can also state the type explicitly. This matters when a variable must be of a specific type, typically in class, component, trait or function hierarchies.

//Declare an Integer - but with no value yet known.
age as Integer: Integer()
//Or like this.
ageAgain as Integer = Integer()
//Or even like this.
ageOnceMore as Integer := Integer()
//Declare an Integer - without type inference
minimumAge as Integer := 21

The form below declares a variable with no memory for a value. Use it for parameters of methods/functions, and avoid it for local variables and for fields/properties of records, classes and components, where it is likely to lead to an error. The exception is an abstract type/trait/class, which you cannot instantiate. EK9 requires the ? suffix on this form, and reading the variable before it is assigned is a compile error (E08020).

//Declare an Integer - but no space allocated
possibleError as Integer?

The different forms come from history, from type inference and from polymorphic variables. Prefer these where possible:

//Local variable declaration and initialisation.
variableName <- TypeConstructor()
//polymorphic variable declaration and initialisation.
variableName as SuperType: SubTypeConstructor()
//Preferred field/property declaration and initialisation.
fieldName as Type: TypeConstructor()
History

EK9 accepts three assignment symbols:

Use the one you are happiest with, but be consistent.

Other assignment and equality operators:

Each of these involves =, as an assignment or as an equality test, so := brings a two-character consistency (in a way). The coalescing operators work well with the 'is set' nature of EK9 variables.

Type Inference

<- declares a variable and infers its type. It is short to write and easy to read.

minimumAge <- 21
//Alternatively - which can be useful
minimumAgeTyped as Integer: 21

The consumer of methods, functions and fields gets type inference, while the writer of records, classes, components, methods and functions declares fields and parameters in full. You see at once which types you are dealing with, and you get the speed and clarity of inference in the bodies of code, where most code is written.

Why EK9 does it this way. Why not use type inference everywhere? A Hindley - Milner mechanism can infer every type, but developers need to see quickly what type they are dealing with, both where a construct is defined and where it is used. An IDE helps (with hovers, for example), but it is much quicker to follow the method call and see the type.

Checking variables

If a variable can exist without a meaningful value, there has to be a way to check it. In other languages you would check for a marker such as -1, or check whether an object is null/nil where the language has that concept (EK9 does not).

EK9 uses the ? (is set) operator:

age <- Integer()
//age exists, but has no meaningful value yet - so 'age?' is false
stdout.println(`age is set: ${age?}`)

age: 21
//Now it has one - so 'age?' is true
stdout.println(`age is set: ${age?}`)

? applies to any type of object. EK9 also has ternary and assignment coalescing operators for variables that may or may not hold meaningful values.

A class or record you write with fields must declare the ? operator (E07235). The same operator applies to generics. Generics are covered later, but they are intrinsic to EK9: the built-in generic types are first class constructs, on a par with Integer and String.

//A simple empty list
aList as List of Integer := List()
if aList?
  //Code where aList has some meaningful value/content
  stdout.println("aList is set, even though it is empty")

For a list with initial values, use the shorthand below (more in collection types).

//A list with some values using the 'list shorthand of' [] - this not an array!
bList <- [ "A", "B", "C" ]

Iterators, another built-in generic type, work the same way. This loop gets each value through an iterator, converts it to a String and prints it on the standard output (console). See the introduction for alternatives to Iterators.

stdout <- Stdout()
bList <- [ "A", "B", "C" ]
iteratorB <- bList.iterator()
while iteratorB?
  stdout.println(`Value is [${iteratorB.next()}]`)

//The stream pipeline construct is cleaner though
cat bList > stdout

In summary: declare a variable and give it space for a value, even when the value is not yet known. A Boolean, for example, has three states: true, false and not yet known.

Assignment

The declarations above combine a variable's name, its type (sometimes inferred) and its initial value in one statement, and that is the best way to create a variable. The <- syntax makes it easy: it is simpler on the eye (low cognitive load), less to type, and it encourages less variable reuse and better names.

To give an existing variable a new value, assign it as below. The ternary operators make a single assignment much more obvious.

//Declare charges and initialise
charges <- 20.99
//Use charges for some processing
// ... processing
//Now set the same variable to a new value
charges := 50.99
//Use updated charges for some processing

The assignment used :=; : or = would do the same. Reassigning a variable can be significant, and some developers would say you should never do it. Here it is with each assignment operator:

//Declare charges and initialise
charges <- 20.99
// ... processing
charges: 50.99
charges = 50.99
//Using := 'feels' more significant
charges := 50.99

EK9 does not stop you reassigning variables unless the block is marked pure; then only the returning value can be reassigned. See pure.

Why EK9 does it this way. pure is the main reason EK9 has no const, final or static, and no immutable versions of its generic collection types: for developers who want immutability, pure is a much stronger concept. EK9 makes the whole block (function/method) pure; if you feel strongly about this, take a more pure approach. Most developers may find that extreme, but EK9 does require pure on some operators to preserve their meaning.

Comments

EK9 has four kinds of comment, shown below. The C/C++/C#/Java /* */ comment is not supported.

One Line Comments

//A one line comment
variable <- 21 //Comments can be at the end of a line if necessary

A single line comment starts with // and runs to the end of the line.

Block Comments

There are two general block comments. The first follows HTML/XML:

<!--
These lines will be contained
within a comment block.
-->

The second is an alternative, more consistent mark-up:

<!-
These lines will be contained
within a comment block, just like above.
-!>

Documentation Comments

This form is used only for documentation comments.

<?-
Just sums the squares of values from 1 to n.
-?>

Why EK9 does it this way. EK9's comments differ from those of many languages to encourage fewer comments: excessive commenting can mislead and adds visual clutter.

Pure Keyword

Controlling variable reassignment is a key idea for developers with a functional programming background, though not a mainstream one for those with an object-oriented or procedural background. It can lead to code that is much less error-prone. This blog post explains why immutability matters, and why it is hard to get right.

It has a cost: sometimes the solution you would traditionally use cannot be applied, and you need to rethink the problem and find another solution.

The function below is marked pure. A 'purist' functional programmer would not call it pure, because of two reassignments, one explicit and one implicit:

#!ek9
defines module site.basics.pure

  defines function

    <?-
      Just sums the squares of values from 1 to n.
    -?>
    sumOfSquares() as pure
      -> n as Integer
      <- sum as Integer: 0
      for i in 1 ... n
        sum: sum + i*i

    //An alternative solution using stream pipeline

    <?-
      Square of number.
    -?>
    square() as pure
      -> number as Integer
      <- squared as Integer: number * number

    streamingSumOfSquares() as pure
      -> n as Integer
      <- sum as Integer: for i in 1 ... n | map with square | collect as Integer

  defines program

    ShowSumOfSquares()
      stdout <- Stdout()
      stdout.println(`Loop: ${sumOfSquares(10)}, stream: ${streamingSumOfSquares(10)}`)

The stream pipeline version is more functional.

The advantage of marking something pure: the code below is a compiler error, because a variable is reassigned in a pure scope. An incoming parameter can never be reassigned, pure or not (E08110).

Fails with E08100: reassignment not allowed when scope is marked as 'pure' (':=?' is supported)

#!ek9
defines module bad.site.basics.purereassign

  defines function

    sumOfSquares() as pure
      -> n as Integer
      <- sum as Integer: 0
      limit <- n
      limit := 20
      for i in 1 ... limit
        sum: sum + i*i

pure can only call pure: from a pure function you cannot call existing functionality that is not marked pure. A function that is not pure can call one that is.

Visibility

Each construct has a different role and purpose, so the visibility of fields/properties and methods varies from construct to construct. This may seem confusing, but it is deliberate.

Module Visibility

All constructs defined in a module (for example defines module introduction) are visible to each other, so function sumOfSquares (above) is visible to any other function or construct in the same module.

References

To use constructs from other modules (and you will) there are two ways. The first is a references statement. Here a constant PI is defined in a module namespace called site.basics.geometry.

#!ek9
defines module site.basics.geometry
  defines constant
    PI <- 3.142

To use PI from another module namespace, site.basics.areas, in a function that calculates the area of a circle:

#!ek9
defines module site.basics.areas
  references
    site.basics.geometry::PI

  defines function
    areaOfCircle()
      -> diameter as Float
      <- result as Float: PI * (diameter/2)^2

The references statement makes PI usable within the module. The alternative is shown below.

#!ek9
defines module site.basics.areas.qualified
  defines function
    areaOfCircle()
      -> diameter as Float
      <- result as Float: site.basics.geometry::PI * (diameter/2)^2

Here the fully qualified name of PI is used. Why have two ways? Sometimes (PI is a bad example) a construct, say a function, has the same name in a different module. You cannot reference it, because the names would clash, so the fully qualified name is the only way to reach it. That is less convenient, especially in several places, so take care how you name constructs and how you link them.

The same syntax works for components, records, types, traits, classes and functions. There are no wildcards: you reference every construct you need from another module, which makes you careful about packaging and reuse. Everything can be referenced: other languages have ways to hide a construct inside a module, but for simplicity EK9 does not.

The component is the main unit of encapsulation in EK9 at large scale; the other constructs are building blocks for components. You can still use a namespace to mark internal constructs that should not be exposed, such as com.geometry.internal, but that is a naming convention, not enforced by the compiler.

Function Visibility

Like constants, all functions are public. Outside the module that defines them they must be referenced or addressed by their fully qualified name: site.basics.areas::areaOfCircle for the function above.

A function name must be unique within a module: you cannot overload functions with different parameters (E01030), unlike methods on classes. A function of the same name can exist in a different module namespace.

Record Field Visibility

Record fields/properties are always public: wherever a record variable is accessible, its fields can be read and modified directly. Records have fields and operators but no methods, so method visibility does not apply. Use pure methods and functions to prevent modification of record fields if you want to.

Records are like structs in other languages, with operators added.

Class Field/Method Visibility

Class fields/properties are always private, so a class that extends a super class has no access to the fields/properties of its super classes (E06180). A field/property cannot be protected or public: the inner workings of a class are private to it. You can write accessor methods to expose them; that is undesirable for encapsulation, but the compiler will not stop you.

All operators are public and can only be public.

Class methods have the most choice:

Trait Method Visibility

Trait methods are always public (an access modifier is E07270). Traits have no fields/properties, so field visibility does not apply.

Component/Field/Method Visibility

As in classes, component fields/properties are always private, and operators are always public.

Component methods are:

Components have no protected access (E07250). They are meant to be composed of other building blocks, such as classes, functions and other components, and extended by composition, not inheritance. If you need complex inheritance hierarchies with components, move that functionality down into classes, and consider the composition mechanisms of classes to make them more reusable.

Why EK9 does it this way. You may consider this advice rather opinionated, and in some ways it is. EK9 has many more specific constructs than other languages, designed to work together in specific ways. Go 'with the grain'; going against it will not end well. If you find this approach too constraining, see the other languages section - you may find a language there that suits you better.

Parameter Visibility

A function or method can read and use its parameters; pure controls whether it may alter them (see the pure modifier).

As described under objects, parameters are always Objects passed by reference, so a function can modify their internal state (subject to the pure modifier): every parameter can be an in out parameter.

That has major implications, good and bad. To return several values from a function or method, you can pass in parameters that you intend to be updated. But you can also modify an incoming parameter by accident. Other ways to return several values are a Dynamic Class used as a 'tuple', or a record.

Why EK9 does it this way. An Object-only approach, with no pass by value, does open the door to such errors. But real-world software mostly works with complex aggregates rather than a few primitive types (like int or float), so this already has to be managed with care. EK9 provides pure so that you can manage value mutation.

Next Steps

To learn which operators exist on the built-in types, and how to implement them in your own classes/records, read operators next.