EK9 Operators
Basics covers declaring variables, comments, the visibility of functions, methods and fields, line termination and indentation (code blocks).
This page lists every operator EK9 supports and what it means, with examples of using them and of overriding them in records and classes. The operators are grouped by related idea. Some behave slightly differently depending on the types, and other languages may use different symbols, so take care. In particular, and/or are both boolean and bitwise operators.
The Operators
- Comparison
- Assignment
- Modification
- Boolean/Bitwise
- Ternary
- Coalescing (is set)
- Mathematical
- Functional
- Pipeline
Most operators will be obvious; a few are new. EK9 makes heavy use of operators because they make code more readable and consistent.
Types of Operator
There are three broad types of operator:
- Unary operators take one operand (such as result := -a or result := a?).
- Binary operators take two operands (such as result := a + b).
- Ternary operators take three operands (such as result := ready <- b : c, which can also be written result := ready <- b else c).
The ternary above reads: "if ready is true, set result to b, else set result to c". Some developers find ternary expressions confusing and fall back to if/else statements; EK9's shortcut syntax, below, may help.
What are Operators
An operator is a shorthand, consistent way of defining a method on an Object; the method is named by a symbol or a phrase. result := a + b could be read as result := a.+(b), but the first form is much more natural.
In the expression below neither 6 nor 8 is modified, as you would expect from mathematics.
result <- 6 + 8
Full source: operators_basics.ek9 - Download operators_basics.ek9
To add something into a value, use +=. + gives a new List with the contents of the existing List plus the value, and leaves both operands unaltered:
//Simple list of string values (note this is shorthand for a List and is not an array) aList <- [ "A", "B", "C" ] //aList remains unmodified as does "D" list2 <- aList + "D" //Now modify list2 - but note the different operator list2 += "E" //Create another list to be used. endList <- [ "X", "Y", "Z" ] //Same syntax and semantics, but different parameters finalList <- list2 + endList //But the result finalList is obvious as ["A", "B", "C", "D", "E", "X", "Y", "Z"]
Full source: operators_basics.ek9 - Download operators_basics.ek9
+ means the same irrespective of type, which generic functions and classes depend on. Overload + and += on your own types rather than inventing names such as 'addTo' or 'addAll', and keep their meaning when you do. Remember too that a simple + may do a lot of processing (adding two Lists may traverse both).
Why EK9 does it this way. Some APIs use a method called add to add something into a value (Java List add), which changes the meaning of addition. Operators that keep their meaning use long-term memory, read quickly, and are made to be used and overridden: for imaginary numbers or a collection, overriding + is strongly encouraged. Design note: an operator keeps its meaning sets out the argument.
Compiler-Enforced Return Value Handling
EK9 separates computational from mutating operators, and the compiler enforces that a computed value is never silently discarded.
- Mutating operators (
+=,-=,:=:,++,--) returnVoid: the side-effect IS the purpose, so there is nothing to discard. - Computational operators (
+,-,*,/,==,<>,contains,matches,length) return values, because the VALUE is the purpose.
A computational operator used as a bare statement, with its result discarded, is
E11050: the statement achieves nothing. This is the principle of Rust's
#[must_use] and C++17's [[nodiscard]], applied automatically to every operator by its
category, with no annotations.
Fails with E11050: operator returns a value but result is discarded - assign to variable or use in expression
//BAD: Computation result goes nowhere - dead code (E11050) price.+(tax)
Full source: operators_discarded_bad.ek9 - Download operators_discarded_bad.ek9
//GOOD: Assign the result total <- price + tax //GOOD: Or use the mutating operator if mutation is the intent price += tax
Full source: operators_discarded.ek9 - Download operators_discarded.ek9
The same principle goes beyond operators: a discarded return from a pure method
(E11051) and a discarded Result/Optional return
(E11052) are compile-time errors too.
See Quality: Purity, Safety, and Contracts.
The Operators themselves
Comparison comes first. Most of it is obvious, but a couple of operators need closer attention (such as the fuzzy match).
Comparison Operators
These operators compare and check values and return a Boolean.
Symbolic Boolean Operators
Typically used in an if statement, these compare the values of two objects and give a Boolean: true or false, or unset if either a or b is unset. That last point is important.
- Less than (a < b)
- Greater than (a > b)
- Greater than or equal to (a >= b)
- Less than or equal to (a <= b)
- Not equal to (a <> b)
- Not equal to (a != b) - if syntax is preferred
- Equal to (a == b)
- Range (a in 1 ... 10) (a not in 1 ... 10)
one <- 1 two <- 2 three <- 3 alsoThree <- 3 six <- 6 eight <- 8 lessThan <- one < two //results in true greaterThan <- one > two //results in false greaterOrEqual <- three >= two //results in true lessOrEqual <- three <= two //results in false notEqual <- three <> alsoThree //results in false alsoNotEqual <- three != alsoThree //results in false equal <- three == alsoThree //results in true inRange <- three in two ... eight //results in true notInRange <- six not in two ... eight //results in false
Full source: operators_basics.ek9 - Download operators_basics.ek9
Comparison Phrase Operators
These two operators are words (phrases) rather than symbols, and return a Boolean:
- Containing (a contains b)
- Matching (a matches b) - uses regular expressions
steve <- "Steve" capitalTe <- "Te" lowerTe <- "te" stevenPattern <- /[S|s]te(?:ven?|phen)/ noMatch <- steve contains capitalTe //results in false hasMatch <- steve contains lowerTe //results in true matchesLeft <- steve matches stevenPattern //results in true matchesRight <- stevenPattern matches steve //also results in true
Full source: operators_basics.ek9 - Download operators_basics.ek9
Symbolic Comparison Operators
These two symbols may be new to you. They return an Integer:
- Comparison (a <=> b)
- Fuzzy Match (a <~> b)
steve <- "Steve" sameName <- "Steve" sameCompare <- steve <=> sameName //results in 0 sameFuzzy <- steve <~> sameName //results in 0
Full source: operators_compare_same.ek9 - Download operators_compare_same.ek9
They look alike but differ in character. <=> is a simple compare; for Strings it is lexical. For your own records and classes you can implement your own comparison, keeping the same meaning.
The fuzzy match <~> is a bridge between the comparison operator and regular expressions. The examples below use Strings, but you can implement your own fuzzy match on your own records and classes.
steve <- "Steve" lowerCase <- "steve" longer <- "Steven" suffix <- "teve" lowerLonger <- "steven" caseCompare <- steve <=> lowerCase //results in -32 fuzzyOne <- steve <~> longer //results in 2 fuzzyTwo <- steve <~> suffix //results in 1 fuzzyThree <- steve <~> lowerLonger //results in 3
Full source: operators_compare_different.ek9 - Download operators_compare_different.ek9
The fuzzy match finds the best match for a value. Here steve is the best match, because only the case of the 's' had to change. EK9's String uses a modified Levenshtein algorithm: a change of case costs 1, replacing one character with another costs 2, and a string with too few characters in common is no match at all.
For your own aggregate types you can use any scoring. It is useful for sparse or incomplete data, and above all for user input, where an address or a name can be matched against a range of known valid entries. You might weight address matching differently for country, city, street and house number: one changed digit in a house number counts for less than the country being "US" rather than "UK". Another fuzzy match might accept 'typos' where the keys are next to each other on the keyboard, to find the word the user meant. Whatever you do, keep the meaning of 'fuzzy'.
With a fuzzy match on your own types you can use and reuse generic functions and classes that rely on fuzzy matching: a generic sorting function that orders items by fuzzy comparison would work with your types as they are.
Symbolic Unary Operator
The operators so far are binary. The next is a postfix unary operator, which checks whether any value is valid:
- Is set (a?)
name1 <- "Bill" name2 <- "Ted" name1Set <- name1? //results in true name3 as String: String() name3Set <- name3? //results in false name4 <- String() name4Set <- name4? //results in false //Note the validity of the calculation is been checked here not the actual result. //The use of the '( ... )' is important here. The ? applies to the result. validCalculation <- (name1 < name2)? //results in true - because the calculation was valid not because Bill is Less than Ted invalidCalculation <- (name1 < name3)? //results in false - because the calculation was invalid name is not set
Full source: operators_isset.ek9 - Download operators_isset.ek9
A record or class with fields must have the is set operator, written by you or generated
with default operator ? (E07235); a class with no fields gets one
generated. It gives a clear, consistent way to tell whether an object holds valid data, so you can create an
object before you know its values.
For example, an Address is an aggregate of house number, street, city and country. To ask a user for it, you create an Address to hold the answers, but it is not valid yet. Only when the user has provided the information can it be checked, and only then is it valid (or not, if the information is wrong).
Consider using records to gather data, then passing them into classes that hold both data and behaviour. The class validates the records during construction, so it has fewer fields and is either fully valid or fully invalid. That means fewer arguments to a class and no need for 'Builders'.
Assignment Operator(s)
These three operators all do exactly the same thing. The different syntax suits developers from different backgrounds, and lets an assignment look natural in different contexts.
- CSS type assignment style (a: b)
- HTML/C type assignment style (a = b)
- Pascal/ADA type assignment style (a := b)
The assign-if-unset operator (a :=? b) is different: it combines an 'if statement' with an assignment. The assignment takes place only if 'a' is absent or unset; then 'a' is made to refer to the same data as 'b'.
Basics explains the three assignment styles; which one feels right in which context is personal preference.
Assignment copies a handle (pointer), not the data. EK9 has no primitives, not even for Integer and Float, so if you alter the value that two or more variables refer to, all of them see that change. That is not what you expect if you are used to primitives. The Address example:
checkAddr <- Address() //a new empty address, so it is unset myAddress <- Address() //assigned to a new empty address myAddress.country: "UK" //assuming the Address was a record with a country field checkAddr := myAddress //Both myAddress and checkAddr reference the same address //But the data has not been copied //Change the country in either checkAddr or myAddress and value will alter checkAddr.country = "US" //myAddress.country is now also "US" anAddress <- Address() //a new empty address, so it is unset anAddress :=? myAddress //anAddress is only assigned if it was unset or had no memory allocated; as above. anAddress :=? Address("US") //anAddress would not have been assign above as it already has a valid Address value
Full source: operators_assignment.ek9 - Download operators_assignment.ek9
The example shows all four assignment operators. To a C# or Java developer this is normal: myAddress and checkAddr both point to the same memory holding the Address data. When you want a full copy of the data, use the modification operators below, ':=:' (copy) and ':~:' (merge). To 'replace' an entry in a collection, ':^:' (replace) gives a shorthand with standard naming.
The assign-if-unset operator (:=?) is a shorthand for an if statement followed by an assignment: a :=? b is exactly a := a ?: b, so b is not even evaluated when a is already set. No assignment yields a value, so an assignment is never an operand (ok <- x := f() does not parse - write x := f() then ok <- x?).
Modification/Mutation Operators
These operators alter the contents of a variable, not only which memory it refers to:
- Add and assign (a += b)
- Subtract and assign (a -= b)
- Multiply by and assign (a *= b)
- Divide by and assign (a /= b)
- Increment (a++)
- Decrement (a--)
- Copy (a :=: b)
- Merge (a :~: b)
- Replace (a :^: b) - normally used with collection
Increment and decrement in EK9:
- Postfix only:
x++andx--. There is no prefix++xor--x. - Statements only:
x++is a statement on its own line. It cannot be used in an expression, soy <- x++is a compile error (E07950). - Mutates in place: the object itself changes, so every variable that refers to it sees the new value.
- Tri-state safe: an unset value stays unset after
++or--.
In C, C++ and Java y = x++ gives the old value; EK9 avoids the question by not allowing
x++ inside an expression.
aFloat <- 22.9 //Create a new floating point value aFloat += 17.1 //Add the value into the variable aFloat //aFloat would now have the value of 40.0 aFloat -= 7.5 //Subtract the value from the variable aFloat //aFloat would now have the value of 32.5 aFloat *= 3 //Multiply aFloat by the value //aFloat would now have the value of 97.5 aFloat /= 2.5 //Divide aFloat by the value //aFloat would now have the value of 39.0 aFloat++ //Increment aFloat by 1.0 //aFloat would now have the value of 40.0 aFloat-- //Decrement aFloat by 1.0 //aFloat would now have the value of 39.0 again bFloat <- Float() //Create a new floating point value but not yet set bFloat := aFloat //Assign the variable and update aFloat++ //Increment aFloat by 1.0, but note bFloat will also change cFloat <- Float() //Create another new floating point value but not yet set cFloat :=: aFloat //Copy the contents of the variable and update aFloat++ //Increment aFloat by 1.0, but note cFloat will not change
Full source: operators_mutation.ek9 - Download operators_mutation.ek9
These operators are fairly straightforward, but the copy ':=:' operator is different in nature:
The copy principle:
- A built-in type copies at its own level. A value (Integer, String, Date, ...) copies its value; a collection (List, Dict, ...) gets its own container holding the same element objects.
- 'default operator :=:' is a shallow copy. After target :=: source each property of the target refers to the same object as the source's - as b <- a and passing a parameter do. Change one of those objects in place and both see it.
- A deep or selective copy is your code - a hand-written operator :=: or a domain method that decides, property by property, to duplicate it, share it or leave it out. Only you know which: renewing an insurance policy for a new period duplicates the named drivers (the new period may add one), shares the insurer's standard wordings and drops the endorsements that have expired. That program is ek9 -q deep or selective copy (Q&A 2100). There is no clone().
The merge rule: ':~:' never overwrites a set value; it fills whatever is unset or missing, element by element, recursing into compatible nested structures; Lists append, Sets take the union. The first value recorded wins. It means the same thing on every built-in type:
- A single value (Integer, String, Date, Duration, Money, Optional, ...) takes the source only when it is unset and the source is set. A set target is kept, whatever the source - so 10#GBP :~: 5#USD leaves 10#GBP.
- A Dict adds each key it lacks, fills each value it holds unset, and keeps each value it holds set - unless both values are the same kind of structure (a Dict of Dict, say), which are merged by this same rule.
- A CSV takes the whole source when it is wholly unset; otherwise it fills only what it lacks: a missing header, a row past its last, a cell that is missing or empty. Stacking rows is += / +, not :~:.
- A List appends the source's items; a Set takes the union. A JSON object follows the Dict rule (a null property is unset); JSON arrays append. A JSON value merged into a JSON value keeps the target, and a value of another kind is never forced in.
Taken means shared, not copied. What the target takes - a key it lacked, an element it held unset - is the same object the source holds, exactly as :=: shares (a shallow copy). Because merge recurses, a later merge into a structure the target took also changes the source's inner object: merge taskOne then taskTwo into an empty Dict, and the inner Dict taken from taskOne receives taskTwo's keys too. To keep every source independent, give the target its own fresh inner structure first, so each merge fills the target's structure rather than taking a source's.
To overwrite, use :=: (copy) or :^: (replace); for anything else, write your own function. A stream into a value (|) is not a merge: cat [1, 2, 3] > total still sums. (A JSON object target is the one place a stream merges: see pouring into JSON.)
Like copy ':=:', merge ':~:' alters the content of the variable, and it is most useful with aggregates. The record below treats fields that are not set as open to replacement. It is a fuller example and uses a couple of operators not yet described. Its :=: is a hand-written copy that calls :=: on each field, so each field copies at its own level (a String copies its value; a List would copy its element references) - deeper than the shallow 'default operator :=:' (see the copy principle).
#!ek9 defines module site.operators.recordmerge defines function //Shows an unset value as '-' so a partly filled Person can still be printed. shown() as pure -> text as String <- rtn as String: text? <- text else "-" defines record Person first <- String() last <- String() dob <- Date() profession <- String() //is set - only when every field is set override operator ? as pure <- rtn as Boolean: first? and last? and dob? and profession? //convert to string operator $ as pure <- rtn as String: `${shown(first)}, ${shown(last)}, ${shown($dob)}, ${shown(profession)}` //equality - written by hand, so the hash code is too, over the same fields operator == as pure -> other as Person <- rtn as Boolean: first == other.first and last == other.last and dob == other.dob and profession == other.profession operator #? as pure <- rtn as Integer: #? first + #? last + #? dob + #? profession //Field-wise copy - calls :=: on each field, each copying at its own level. //'default operator :=:' is a SHALLOW copy - each field refers to the same object as the source's. //Write your own :=: like this when you need a deeper or selective copy. operator :=: -> other as Person //No 'if other?' guard: a partly filled Person is not 'set', but its fields must still be copied. first :=: other.first last :=: other.last dob :=: other.dob profession :=: other.profession //merge - fill only what is unset, each with its own copy operator :~: -> other as Person //No 'if other?' guard here either: it would skip every partly filled Person. if not first? first :=: String(other.first) //Could be written like this with assign-if-unset. last :=? String(other.last) dob :=? Date(other.dob) profession :=? String(other.profession) defines program //quick program to show how operators on Person can be used. PersonMerge() stdout <- Stdout() john <- Person() john.first: "John" john.profession: "IT" smith <- Person() smith.last: "Smith" smith.dob: 1960-01-01 //So lets merge these into a new variable merged <- Person() merged :~: john merged :~: smith //Alter first name john.first: "Jon" //A copy of the merged record, then just show the values. copied <- Person() copied :=: merged stdout.println(`[${john}] set: ${john?}`) stdout.println(`[${smith}] set: ${smith?}`) stdout.println(`[${merged}] set: ${merged?}`) stdout.println(`copied == merged: ${copied == merged}`)
The output:
[Jon, -, -, IT] set: false [-, Smith, 1960-01-01, -] set: false [John, Smith, 1960-01-01, IT] set: true copied == merged: true
Because the merge was done before the first name was altered to "Jon", the merged record holds its own copy of that value.
Neither :=: nor :~: starts with if other?. It is tempting to write that guard, but ? on 'Person' is only true when every field is set, so a partly filled 'Person' (such as 'john' or 'smith' above) is not set, and a guarded copy or merge from one would silently do nothing. Each operator works field by field instead, and each field's own operator deals with an unset value. The '$' operator shows an unset field as '-' (through the small function 'shown'), so a partly filled record can still be printed. Because '==' is written by hand, so is '#?', over the same fields.
Boolean (Logical) and Bitwise Operators
These operators are used by two different types, with broadly the same meaning. Other languages model them in different ways; EK9 uses the same syntax in both contexts.
Boolean (Logical) Operators
The logical operators work on Boolean values:
- Not (a = ~b)
- Not (a = not b)
- And (c = a and b)
- Or (c = a or b)
- Xor (c = a xor b)
'~' is the same as 'not'. Not many languages have 'xor' (it can be built from 'and' and 'or').
//Simple boolean values a <- true b <- true c <- false //Each of the operators //The and/not operator andAB <- a and b //result would be true andAC <- a and c //result would be false andNotC <- a and not c //result would be true andTildeC <- a and ~c //result would be true //The or/not operator orAB <- a or b //result would be true orAC <- a or c //result would be true notAOrC <- not a or c //result would be false tildeAOrC <- ~a or c //result would be false //The xor/not operator xorAB <- a xor b //result would be false xorANotB <- a xor not b //result would be true xorAC <- a xor c //result would be true notAXorC <- not a xor c //result would be false tildeAXorC <- ~a xor c //result would be false
Full source: operators_logic.ek9 - Download operators_logic.ek9
Bitwise Operators
The bitwise operators work on bits. They look the same as the logical operators, with two more (shift left and right). They work on sets of bits: not ints or longs, but an ordered collection of bits.
- Not (a = ~b)
- Not (a = not b)
- And (c = a and b)
- Or (c = a or b)
- Xor (c = a xor b)
- Left Shift (c = a << b)
- Right Shift (c = a >> b)
The least significant bit is on the right and the most significant on the left, as you would expect.
//Simple bits values a <- 0b010011 b <- 0b101010 c <- 0B01010011 //Each of the operators //The not operator notA <- not a //result would be 0b101100 //The and operator andAB <- a and b //result would be 0b000010 //The or operator orAB <- a or b //result would be 0b111011 //The xor operator xorAB <- a xor b //result would be 0b111001 //The << operator shiftedOne <- c << 1 //result would be 0b010100110 shiftedTwo <- c << 2 //result would be 0b0101001100 //The >> operator shiftedRight <- c >> 2 //result would be 0b010100
Full source: operators_logic.ek9 - Download operators_logic.ek9
A set of bits is not a long, an int or any other primitive type, and it can grow (as the shift left shows). You can join sets of bits and use them in pipeline processing to check specific values, but they are not a primitive type nor an array of bits (there are no arrays). See built-in types and Standard Types.
Ternary style Operators
Several ternary-style operators have a shorthand, and some are new; the end of the coalescing section shows why they were introduced. The main ternary operator will be familiar:
//Setup values bird1 as String: String() bird2 <- "Duck" answerA <- bird1? <- bird1 : bird2 //answerA would have the value of "Duck" because 'bird1' is not set //Note this could have been written as answerA := bird1? <- bird1 else bird2 //Maybe this is less terse but more readable and obvious. bird1 := String() answerA := bird1? <- bird1 : bird2 //answerA would still have the value of "Duck" because 'bird1' does not have a meaningful value bird1 := "Goose" answerB <- bird1? <- bird1 : bird2 //answerB would now have the value of "Goose" because bird1 is set
Full source: operators_ternary.ek9 - Download operators_ternary.ek9
Any Boolean expression can take the place of 'bird1?'. 'else' in place of ':' makes the ternary more obvious and readable.
Coalescing Operators
The coalescing operators work on variables that may be unset:
- Coalesce (Elvis) (c = a ?: b) - b is evaluated only when a is unset
- Comparison coalescing, less than (c = a <? b) - returns one of its operands, the left one when they are equal; a and b are both evaluated
- Comparison coalescing, greater than (c = a >? b) - returns one of its operands, the left one when they are equal; a and b are both evaluated
The Elvis operator '?:' checks that its left operand is present and set. There is no '??' null coalescing operator as in C#, JavaScript or Swift (E01096): EK9 is designed so that a value is never absent, though it may be unset, so '??' would have nothing to test, and '?:' is the operator that supplies a value when the left is unset.
Every coalescing operator (and the ternary) returns one of its operands, so the operands must share a
type: the same type or a subtype, one operand that promotes (#^) to the other's type
(count ?: 2.5 is a Float, and only the operand chosen is promoted), or their nearest
common super class or trait. Operands that share nothing but Any are a compile error at the
operator (E50030) - never an invented Any.
The coalescing operators ?: and <? >? share one level that binds
tighter than arithmetic, comparison and logic. Other languages disagree about where they sit, so EK9 never lets
one of them mix with a different binary operator without parentheses
(E08269): write (a + b) ?: c or
a + (b ?: c), never a + b ?: c. A chain of the same
operator, a ?: b ?: c, needs none.
A unary operator binds tighter than any binary operator, the coalescing ones included:
-x ?: 5 is (-x) ?: 5, ~ready ?: true is
(~ready) ?: true, $count ?: "none" is ($count) ?: "none" and
flag ?: text is empty is flag ?: (text is empty). An unset operand of -
or ~ gives an unset result, so the coalescing operator then supplies its fallback. There are two
exceptions, both the usual convention: unary minus binds below ^ (-x ^ 2 is
-(x ^ 2), and so is -2 ^ 2: a literal is no exception, so it is -4 - write
(-2) ^ 2 to square the negative number), and the word not binds below the comparisons
(not age == limit is not (age == limit), not name in names is
not (name in names) and not ready ?: true is not (ready ?: true)).
Every postfix operator binds above unary minus, so -x is empty is -(x is empty).
Read from the right, the word not would not reach past an operator before it, so
ready == not done is a compile error (E08273), as it is in
Python: write ready == (not done), or not (ready == done) if that is meant. It applies
to every binary operator above not; ready and not done, ready or not done and
ready xor not done have one reading and need no parentheses. Every prefix operator binds tighter
than the word not as well, so $ not done and #? not done are errors too:
write $ (not done). In one sentence: the word not
is never an operand of another operator unless parenthesised, except and, xor and or.
~ is the tight negation: ready == ~done is ready == (~done).
Word not, ~ and unary - are the same negation, so a negation applied to a negation
cancels out: not not done, ~~done, not ~done, - -amount,
-not done and, parenthesised, not (not done), ~(~done) and
-(-amount) are all compile errors (E08274). Write the operand,
or a single negation if one was meant - EK9 cannot tell which, so it does not choose for you. Subtraction is
not a negation: -(first - second) and first - -second are legal.
Operator precedence
EK9 follows the convention of C, Java and Python. From the tightest binding to the loosest:
| Level | Operators | Example and its reading |
|---|---|---|
| Postfix and prefix | x++ x-- x! x?, $ $$ #^ #< #> #? length abs sqrt ~, is empty |
$count ?: "none" is ($count) ?: "none" |
| Power (right-associative) | ^ |
2 ^ 3 ^ 2 is 2 ^ (3 ^ 2), 512 |
| Unary minus | -x |
-x ^ 2 is -(x ^ 2), and -2 ^ 2 is -(2 ^ 2), -4; -x * 2 is (-x) * 2 |
| Coalescing (right-associative) | ?: <? >? |
a ?: b ?: c is a ?: (b ?: c); a mix with any other operator needs parentheses (E08269) |
| Multiplicative | * / mod rem |
a + b * c is a + (b * c) |
| Additive | + - |
a - b + c is (a - b) + c |
| Shift | << >> |
bits << n + 1 is bits << (n + 1) |
| Comparison | <=> <~> |
a <=> b |
| Relational and membership | < <= > >= matches contains in, in a ... b |
name in names == expected is (name in names) == expected;
valid and age in 18 ... 65 is valid and (age in 18 ... 65) |
| Equality | == <> != |
a < b == c is (a < b) == c |
| Logical not | not |
not age == limit is not (age == limit); not a and b is (not a) and b; a == not b needs parentheses (E08273), a and not b does not |
| Logical and, then xor, then or | and, xor, or |
a or b and c is a or (b and c) |
| Ternary (right-associative) | <- ... else |
ok and ready <- first else second is (ok and ready) <- first else second |
The assign-if-unset operator (:=?) is not a ternary, but it is related: it is the assignment form of the Elvis operator - a :=? b is a := a ?: b. The right-hand side is evaluated only if the left-hand side is unset (or absent). Either way it leaves an object in its target - present, maybe unset, never absent - so it initialises a variable declared with no value.
To do the opposite - take a new value only when it is set, and otherwise keep the one you have - write x := f() ?: x. A plain assignment will not do that: x := f() always stores the result, even an unset one.
//Setup values - bird1 is declared with no value bird1 as String? bird1 :=? "Goose" //bird1 would have the value of "Goose" because 'bird1' has no value yet //In contrast. bird2 <- initialBird //initialBird has the value "Duck" bird2 :=? "Goose" //bird2 would have the value of "Duck" because 'bird2' was originally set
Full source: operators_assign_if_unset.ek9 - Download operators_assign_if_unset.ek9
Why not use if statements and the normal comparison operators? Those give an unset result if either value is unset: comparing a value with an unset one is meaningless. These operators accept that either value, or both, may be unset.
//Initialise values bird1 as String: String() bird2 <- "Duck" answerA <- bird1 <? bird2 answerA := bird1 <? bird2 answerA := bird1 >? bird2 //In all above examples answerA would have the value of "Duck" because 'bird1' is not set //As you can see bird1 is not less than, equal to or greater than bird2 because bird1 not set. answerB <- bird1 ?: bird2 //answerB has the value of "Duck" because 'bird1' is not set answerB := bird1 ?: bird2 //answerB again has the value of "Duck" - '?:' replaces a value that exists but has no meaning yet //By the same logic answerB would have the value of "Duck" when the <?, >? operators are used. bird1 := "Goose" answerC <- bird1 ?: bird2 //answerC would now have the value of "Goose" because bird1 is set answerC := bird1 <? bird2 //answerC would have the value of "Duck" answerC := bird1 >? bird2 //answerC would have the value of "Goose"
Full source: operators_coalescing.ek9 - Download operators_coalescing.ek9
These operators make it easier to process variables that may or may not have been given a meaningful value, and simplify comparisons where some values may not be set.
'<?' and '>?' follow the same logic as the Elvis operator '?:' (as does the assign-if-unset ':=?'), but they always evaluate both operands and return one of them. They work whether an operand is absent, unset or holds a meaningful value.
Assign-if-unset Operator
In short:
- To assign the result of comparing two values that may not be meaningful, use '<?' and '>?' rather than '<', '<=', '>' and '>=', unless you need to keep the fact that the comparison was meaningless.
- To assign a variable only if it is currently unset, use ':=?'.
One of the most important concepts in EK9 is that a variable can exist but not hold a meaningful value. Most developers expect a comparison of two values to give one of three results:
- Less than
- Greater than
- Equal to
There is a fourth: one or both values is unset, and a standard comparison is then meaningless. These operators let you compare values while allowing for that. When both values are meaningful, the normal comparison applies.
Comparison Coalescing Semantics
The comparison coalescing operators follow a priority cascade:
- If the left operand is absent or unset → return the right operand
- Else if the right operand is absent or unset → return the left operand
- Else both are valid → compare them and return the appropriate one
- Default → return the right operand
So a valid value is always preferred over an absent or unset one; when both are invalid, the right operand is returned (a consistent fallback); and when both are valid, the normal comparison applies. (EK9 is designed so that a program can never observe an absent value; the operators treat one as unset.)
Truth Table for <? (Lesser Value)
| x state | y state | Rule Applied | Result |
|---|---|---|---|
| absent | absent | x invalid | y |
| absent | unset | x invalid | y |
| absent | set | x invalid | y (use valid value) |
| unset | absent | x invalid | y |
| unset | unset | x invalid | y |
| unset | set | x invalid | y (use valid value) |
| set | absent | y invalid | x (use valid value) |
| set | unset | y invalid | x (use valid value) |
| set | set | compare | x if x<=y (a tie gives x), else y |
>? follows the same validity checks; only the final comparison differs when both operands are
valid: x if x>=y, else y.
When the two values are equal (their '<=>' gives 0), <? and
>? both return the left operand. For an Integer or a String the two are
indistinguishable, but for objects that '<=>' ranks equal and that differ in other ways it is
the left one that wins; swap the operands to get the right one. There are no 'or equal' forms:
<=? and >=? were removed (E01095), because this is the
one tie rule.
The comparison is the type's '<=>', so the type needs one (E07620). On an open or abstract class, record or component, or a trait, two values may be of different run-time types, and their '<=>' is then unset: the operators are a compile error there (E07187) unless the '<=>' is written by hand on the abstract base or trait, which orders the whole hierarchy. Closed types are not affected.
Note: a coalescing expression whose operands are both literals is reported as tautological (E08094), because its result is fixed at compile time. So is a literal on the left of ?:: it is always set, so the right would never be used.
These operators may seem like yet another thing to learn, but without them you would write the following:
//Values to work with bird1 <- functionToGetBird1() bird2 <- functionToGetBird2() //Without using new operators answerA <- String() if not bird1? answerA := bird2 else if not bird2? answerA := bird1 else answerA := bird1 < bird2 <- bird1 else bird2 //With the new operator answerA := bird1 <? bird2 //With the new operator and function calls answerA := functionToGetBird1() <? functionToGetBird2() //Without the new operators, you would want to avoid multiple calls!
Full source: operators_coalescing_benefit.ek9 - Download operators_coalescing_benefit.ek9
So these operators are more than a null-safe version of the normal comparators: they mix the Elvis operator with comparison and assignment, and remove a lot of null and if/else code. They also work with type inference: in the example above birdA is declared, type inferred and assigned in one go.
Finally, imagine bird1 and bird2 were calls to functions or methods rather than variables. Without the coalescing operators you would need extra variables to capture the results; with them you do not.
Mathematical
Addition '+' and subtraction '-' can and should also be used with other types, records and classes. For a collection such as a List, '+', '+=', '-' and '-=' make sense: they are logical and keep their meaning.
- Addition (c = a + b)
- Subtraction (c = a - b)
- Multiplication (c = a * b)
- Division (c = a / b)
- Factorial (c = a!)
- Power (c = a ^ b)
- Modulus (c = a mod b)
- Remainder (c = a rem b)
- Absolute (c = abs a)
- Square root (c = sqrt a)
//Initialise values a <- 21.9 b <- 13.6 c <- Float() d <- Float() //Addition examples e <- a + b //'e' would have the value of 35.5 e := b + a //'e' would again have the value of 35.5 //But in these following cases 'c' is not set and so the result is also not set e := a + c e := c + a //Also in these following cases both 'c' and 'd' are not set and so the result not set e := c + d e := d + c //Subtraction examples e := a - b //'e' = 8.299999999999999 e := b - a //'e' = -8.299999999999999 e := a - c //'e' = not set e := c - a //'e' = not set e := c - d //'e' = not set e := d - c //'e' = not set //Multiplication examples e := a * b //'e' = 297.84 e := b * a //'e' = 297.84 //all other combinations involving 'c' or 'd' result in 'e' being not set //Division examples e := a / b //'e' = 1.6102941176470587 e := b / a //'e' = 0.6210045662100457 //all other combinations involving 'c' or 'd' result in 'e' being not set //Factorial examples alpha <- 5 e := alpha! //'e' = 120.0 //Power examples beta <- 5 e := beta ^ 2 //'e' = 25.0 e := beta ^ 3 //'e' = 125.0 e := beta ^ 4.8 //'e' = 2264.936448992798 //Modulus and remainder examples charlie <- -21 f <- charlie mod 4 //'f' = 3 because -21 + 4 x 6 is 3 f := charlie rem 4 //'f' = -1 because -21 / 4 gives -5 with a remainder of -1 //For positive values the results are the same for modulus and remainder delta <- 21 f := delta mod 4 //'f' = 1 f := delta rem 4 //'f' = 1 //Absolute - abs example f := abs charlie //'f' = 21 f := abs delta //'f' = 21 echo <- -23.88 e := abs echo //'e' = 23.88 //Square root - sqrt example e := sqrt delta //'e' = 4.58257569495584 e := sqrt abs echo //'e' = 4.886716689148247 //Note the combination of sqrt and abs. e := sqrt echo //'e' = not set cannot sqrt a negative number //EK9 does not have imaginary numbers so sqrt of a negative number is not valid
Full source: operators_mathematical.ek9 - Download operators_mathematical.ek9
Functional
EK9 has a number of operators most other languages lack. They encourage standard naming and readability, and model common ideas: conversion to a String or a Hash code, and promotion from one type to another (for example Integer to Float).
Another common idea is closing things, such as connections and files, where the end of use has to be marked. A type that needs closing defines operator close, and you never call it yourself (E81060); there is no close statement either (E01094). A try block with resources closes them for you, as described in Exceptions and Error Handling, so managed resources are handled tersely and consistently.
EK9 also has an operator to convert built-in types, records and classes to JSON. On your own types it can usually be defaulted, but sometimes you need to write your own.
- Empty (b = a is empty)
- Length (b = length of a)
- Convert to String (b = $ a)
- Convert to JSON object (b = $$ a)
- Hash code (b = #? a )
- Promote (b = #^ a)
- Prefix (b = #< a)
- Suffix (b = #> a)
Words such as is and of in some operators are sugar: you can leave them out, though they often make the code more readable.
//Initialise values anInteger <- 106 aList <- ["V1", "V2", "V3"] //Remember this is a List not an Array aString <- "A-to-Z" money <- 350.50#USD //Examples of use with a List of Strings isEmpty <- aList is empty //isEmpty will have value of false, because the list is not empty isNotEmpty <- aList is not empty //isNotEmpty will have value of true, because the list is not empty //The above could have been written as isNotEmpty := not aList empty //But this reads like Yoda might have said it. //Length example listLength <- length of aList //listLength will be 3 //The above could have been written as listLength := length aList //The 'of' is just to add readability //Convert List to a String and get its length listAsString <- $ aList //listAsString will be ["V1", "V2", "V3"] lengthOfListAsString <- $ length of $ aList //lengthOfListAsString will be 18 //Accessing specific items in the List safely with getOrDefault v1 <- aList.getOrDefault(0, "") //v1 will be "V1" v3 <- aList.getOrDefault(2, "") //v3 will be "V3" //Access by index with a safe default for out-of-range indices maybeAString <- aList.getOrDefault(5, "") //maybeAString will be "" - the default, as there are only 3 items //getOrDefault always returns a value: either the element at the index, or the provided default //No exceptions, no unset results - safe by design //But the pipeline processing is more powerful v2 <- cat aList | skip 1 | head 1 | collect as String //v2 will be "V2" //Pipeline processing is also safe by avoiding indexing nonExistent <- cat aList | skip 4 | head 1 | collect as String //nonExistent will be not set as there are only 3 Strings in the list //Same syntax can be used with Strings (or your own records/classes if you provide operators) isEmpty := aString is empty //isEmpty will have value of false, because the string has a value of "A-to-Z" firstChar <- #< aString //firstChar will be 'A' lastChar <- #> aString //lastChar will be 'Z' //The important point here is the same syntax works with different types like String, Integer, Money //Further example shown using Money amount <- #< money //amount will be 350.5 currency <- #> money //currency will be USD //The following examples outline hash code and promotion of type aFloat <- #^ anInteger //aFloat will be 106.0 (i.e a floating point number) //With your own records and classes you can provide your own promotions hashCode <- #? anInteger //hashCode will be 106 hashCode := #? aFloat //hashCode will be 1079672832
Full source: operators_prefix.ek9 - Download operators_prefix.ek9
Pipeline operator
The pipe '|' operator is used only with streams and pipeline processing. Built-in types provide it where it fits (List, Integer and Float, for example) to accumulate values, and you can override it in your own Records and Classes.
It is similar to '+=', with one significant difference: '+=' typically does nothing to a value that is unset, but '|' does. So a value can go from unset to an initial value on the first '|'. Later calls should accumulate or replace the value, depending on the type.
See Integer type in a pipeline for an example.
Pouring into JSON follows the target's shape: an unset or array target collects, an object target merges each streamed object in; anything else - a non-object into an object, or anything into a single value - is invalid, leaves the target unset, and later items do not revive it: a mismatch anywhere in the stream is still visible at the end, until the target is assigned again (:=:, :~: or clear()) or replaced by a '>' stream. A '>' stream pours into a copy and replaces the target when it ends, so a mismatch there leaves the target plain unset, and a later stream into it collects again. An array target takes each item as one element (an array poured in is not spread; a '>' stream replaces the array with the items it collects, a '>>' stream adds them), an object target merges by the merge rule (the first value recorded wins), and a JSON null item is unset, so it is dropped. See JSON.
Defaults
Your language or tool chain (Java's Lombok, for example) may provide many of these operators for you. EK9 does too, in a layered and controlled way, on records, classes and components. See Records and Classes.
EK9 can generate operators such as '$', '$$', '#?' and '?' from the fields/properties of the type and of any super record/class. Those fields must have the operators too, or the compiler reports an error.
The converse is not generated: EK9 does not write the code that turns JSON back into a record or class. You write that yourself.
Sameness: comparison and equality
A type defines its sameness once, with either '<=>' (ordered) or '==' (equality only), written by hand or with default:
- From '<=>' EK9 generates '==', '<>', '<', '<=', '>' and '>='. From '==' alone it generates '<>'; the type then has no ordering.
- Writing or requesting a generated comparison yourself is a compile error (E07181).
- Per-type sameness - the usual kind - is the type's own: a sub-type with no fields of its own inherits it, one that adds fields declares its own over all its fields (E07238), and values of different run-time types are never the same - '==' is false and '<=>' unset.
- Sameness written by hand on an abstract base or a trait is hierarchy-wide sameness - how different kinds compare - and a sub-type cannot redefine it (E07182). A defaulted one on an abstract base is per-type, like any other type's.
- '#?' must agree with the sameness: a defaulted sameness gets its '#?' generated, and a hand-written one needs a hand-written '#?' over the same fields (E07184).
The generated '<=>' works through the fields:
- If either object is unset under its own ?, the result is unset.
- If the type has a super, the super's '<=>' is called first; only if it gives 0 are the fields of the current type compared.
- Fields are compared in declaration order; the first that differs decides the result.
- If any field comparison is unset, the result is unset (incomparable).
- The result is 0 only when every comparison gives 0.
So comparisons between objects whose fields are not all set propagate the uncertainty. To change this, write your own '<=>' at the root.
'default operator' lets you provide all the operators at once, or a finite list of them, and write your own implementations as and when you need to.
Operators Supporting Default
The operators below are provided on records and classes: some by declaring them default, the comparisons by generating them from the type's sameness.
| Operator | Method | Return Type | Description |
|---|---|---|---|
| <=> | _cmp | Integer | Three-way comparison (returns -1, 0, or 1); may be default |
| == | _eq | Boolean | Equality check; may be default as the sameness, otherwise generated from <=> |
| <> | _neq | Boolean | Not equals; generated from the sameness, never requested (E07181) |
| < | _lt | Boolean | Less than; generated from <=> (E07181 if requested) |
| <= | _lte | Boolean | Less than or equal; generated from <=> (E07181 if requested) |
| > | _gt | Boolean | Greater than; generated from <=> (E07181 if requested) |
| >= | _gte | Boolean | Greater than or equal; generated from <=> (E07181 if requested) |
| ? | _isSet | Boolean | Check that ALL fields are set (and super.?) |
| $ | _string | String? | String representation, Type(Super(...), field=value); UNSET if any field (or the super) is unset, or if there is nothing to render - no properties anywhere in the type and its supers |
| $$ | _json | JSON | JSON object of the properties, inherited ones included (an unset property is JSON null); UNSET only if there is nothing to render |
| #? | _hashcode | Integer? | Hash code (UNSET when the object is unset under ?) |
Default Operator Semantics
The default operator ? returns true only if EVERY field is set - as if the whole inheritance chain were one object. A partly set object is unset. For types with inheritance, the super's ? is checked first: if it returns false, so does the derived class.
Note on Any: all EK9 types implicitly extend Any, and Any's ? always returns false, so the default ? does not call the super's ? when the immediate super is Any. A class with no explicit parent checks only its own fields; when you extend another class, that parent's ? is checked first.
Types with no fields: a class, component, service or dynamic class with no fields at all
(counting inherited ones) is set whenever it is present - the compiler gives it a ? that returns
true, so you do not write default operator ?. A ? the type writes, or inherits from a
super or trait, is never replaced, and a sub-type that adds fields still defines its own. Because such a
value is always set, checking a fresh one (s <- Stateless() then if s?) is
reported as always true (E08088). Records are different: a record exists only
to carry data, so a concrete record with no fields, counting inherited ones, is a compile error
(E07237); only an abstract record, the root of a record hierarchy, may
have none.
What the generated #?, $ and $$ check first. The generated #? asks the object's own ? (yours, if you wrote one): an object that is unset under ? has no hash, so #? is UNSET. The generated $ and $$ never consult ? - they render whatever data is present, so a custom ? for domain validation does not change them. $ is UNSET when any field it renders (or the super's rendering) is unset; $$ renders an unset property as JSON null.
The compiler decides whether there is anything to render, from the type itself - not at run time. A type with
no properties of its own and no super that contributes any (no properties anywhere in its chain) has nothing
to render: its generated $ and $$ return UNSET. A sub-type with no properties of its own
below a super that has some renders the super's, nested - for example Named(Person(name=Ada)) and
{"name":"Ada"}. A super whose $ or $$ is abstract contributes nothing: there is
nothing there to call.
An abstract $, $$ or #? only where there are no fields. Because a generated operator below an abstract one covers only the sub-type's own fields, an abstract $, $$ or #? on a type that HAS fields (its own or inherited ones) is a compile error (E07185), reported at the abstract operator: those fields would silently be left out. Give it a body or default operator $ where the data is (a hand-written body when the fields are not to be shown). A field-less abstract root of a hierarchy, or a trait, may keep the operator abstract - there is nothing to lose. (== and <=> are never abstract, E07183; ? always overrides Any's, so it cannot be abstract either.)
The nearest operator above, and a sub-type that adds fields. A generated operator folds in the
nearest implementation above its type - the super's, or one further up the chain when the types in
between declare none - so a type with no fields in the middle of a hierarchy needs no operators of its own and
adds no layer: Leaf(Base(id=1), name=leaf). A type that adds fields must declare its own
$, $$ and :=: wherever it would otherwise inherit one
(E07238), as it must declare ? (E07235): the inherited operator was
written for the super's fields and cannot know the new ones. The same goes for sameness, on a
concrete or an abstract type with per-type sameness - <=> below an ordered super, == below an equatable-only one, with
#? (E07238 again): inherited, it would compare only the super's fields, so two values that differ
only in the new ones would be the same. default operator <=> compares the super's first, then
the new fields. A sameness written by hand on an abstract base or a trait is the exception: it is hierarchy-wide,
and its sub-types inherit it (a defaulted one there is per-type). So an abstract type
that adds fields chooses: default operator gives it a per-type sameness, and a <=>
written by hand on it is hierarchy-wide, sealed for every sub-type.
Summary
Most operators, especially the logical and mathematical ones, are obvious. Others handle common operations such as type promotion, hashing and string conversion tersely.
The rest deal with variables that may or may not be set. A variable without meaningful data is an important concept in EK9, which is why these operators exist. The null safe ternary, coalescing and assign-if-unset operators take some getting used to: when you check with an 'if statement' whether a value is set, look for a built-in operator that shortens the code.
Next Steps
Built-in types has more detail on operators; read it next. Structure covered the overall layout and constructs, and this page covered the operators.