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
- Indentation
- Line Termination
- Parameter Passing
- Operators
- Objects and no Primitives
- No auto boxing (and why)
- Declarations
- Variable Declarations
- Different assignment symbols (and why)
- Type Inference
- Checking if a variable has a value!
- Assignment
- One Line Comments
- Block Comments
- Documentation Comments
- Pure and re-assignment
- Visibility
- Module Visibility
- Function Visibility
- Record Visibility
- Class Visibility
- Trait Visibility
- Component Visibility
- Parameter access
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
Download basics_parameters.ek9
- One parameter: prefer the single line, -> n as Integer (function1).
- Two or more: -> goes on a line by itself, with the parameters on the lines below it, indented (function2). A comma-separated list on the -> line does not parse.
- The returning value should follow the same layout as the parameters, single line (function1) or indented (function2). This is a convention, not a rule.
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:
- the caller's String is never modified;
- the function receives a clean, sanitized copy;
- malicious content is removed or escaped.
Restrictions:
sanitizedapplies only to incoming String parameters.- It cannot be used on fields/properties of classes, records or other constructs; use the Sanitizer component to sanitize a field.
- It cannot be used on returning values or local variables.
- A sanitized parameter cannot be assigned directly to a local variable, to keep the copy semantics: use the
parameter itself, or make an explicit copy with
String(param). - An override may add
sanitizedto a parameter, but never drop it from a parameter the super method or function markssanitized(the Liskov Substitution Principle).
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
Download basics_sanitized_alias.ek9
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
Download basics_sanitized_copy.ek9
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 valuename <- String() →
unsetname <- "Steve" → setname <- "" →
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 emptyitems <- 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 SETList()? is true |
| "A new String is set" | String() is UNSETString()? is false (the literal
"" is set) |
| "Dict with 0 items is unset" | Empty Dict is SETDict()? is true |
| "Only unSet() makes things unset" | Invalid construction also creates unsetInteger("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?":
- Unset: the object exists but holds no meaningful value yet - a String created and awaiting input, or the default handed back when a key is not in a dictionary (as in the example below)
- Set: the object holds a valid, meaningful value, ready to use
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}` Full source: basics_tristate.ek9 - Download basics_tristate.ek9 |
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:
- String: it has been given text (
String()is unset,""is set) - Integer: it has a numeric value
- List: it exists (even if empty - empty is a valid state)
- Dict: it exists (even if empty)
- Optional of T: it holds a value, and that value is set
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)
Full source: basics_tristate.ek9 - Download basics_tristate.ek9
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
- At first, empty collections being "set" will confuse you. Remember: collections are always set when created - empty is a valid state.
- Then you use the
?operator naturally to check whether values are set, and stop writing!= nullchecks. - Later you value the precision: you can tell "key not found" from "lookup failed" from "result has no data", states that Java and Python collapse into one ambiguous null. Guards, and the compiler's refusal to read a value that may not be there, take the place of null checks.
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
- Assignment and Guards - How to work with tri-state values safely
- Guard Operator Quick Reference - Using guards with tri-state semantics
- Control Flow Philosophy - How tri-state enables guards
- Collection Types - Why collections are always set
- Optional Type - A container whose state depends on what it holds
- Code Quality - How tri-state impacts quality enforcement
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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?
Full source: basics_variables.ek9 - Download basics_variables.ek9
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:
- : - CSS and JSON use it to initialise data elements. It is clear to read, and many programmers are used to it.
- = - HTML uses it for properties of UI elements, and most programming languages use it to assign variables.
- := - Pascal and Ada (among others) use it. It is consistent with the other operators listed below, and it looks and feels stronger as an assignment than : or = alone.
Use the one you are happiest with, but be consistent.
Other assignment and equality operators:
- :=? Assign-if-unset - fills the variable only if it is unset
- == Test for equality
- <= Less than or equal
- >= Greater than or equal
- <> Not equal to (like SQL not equals)
- != Also not equals to (like C, C++, Javascript not equals)
- += Add value and assign to
- -= Subtract value and assign to
- *= Multiply value and assign to
- /= Divide by value and assign to
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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?}`)
Full source: basics_variables.ek9 - Download basics_variables.ek9
? 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")
Full source: basics_variables.ek9 - Download basics_variables.ek9
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" ]
Full source: basics_variables.ek9 - Download basics_variables.ek9
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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
Full source: basics_variables.ek9 - Download basics_variables.ek9
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. -->
Full source: basics_variables.ek9 - Download basics_variables.ek9
The second is an alternative, more consistent mark-up:
<!- These lines will be contained within a comment block, just like above. -!>
Full source: basics_variables.ek9 - Download basics_variables.ek9
Documentation Comments
This form is used only for documentation comments.
<?- Just sums the squares of values from 1 to n. -?>
Full source: basics_variables.ek9 - Download basics_variables.ek9
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:
- sum - reassigned; EK9 allows this because it is the returning variable.
- i - the loop variable is incremented; EK9 allows this as well.
#!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
Download basics_pure_reassign.ek9
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
Download basics_areas_qualified.ek9
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:
- private - only accessible within the class defining it.
- protected - accessible to the class defining it and all its subclasses.
- public - accessible by any caller. This is the default: write no modifier (writing public is E07280).
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:
- private - only accessible within the component defining it.
- public - accessible by any caller.
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.
- Reassigning a parameter with ':', '=' or ':=' is a compile error (E08110), pure or not.
- Mutating it, with an operator such as '+=' or '++', a mutator method, the copy operator ':=:' or the merge operator ':~:', alters the object itself, so the caller sees the change.
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.