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

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:

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

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"]

+ 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.

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)
//GOOD: Assign the result
total <- price + tax
//GOOD: Or use the mutating operator if mutation is the intent
price += tax

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.

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
Comparison Phrase Operators

These two operators are words (phrases) rather than symbols, and return a Boolean:

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
Symbolic Comparison Operators

These two symbols may be new to you. They return an Integer:

steve <- "Steve"
sameName <- "Steve"
sameCompare <- steve <=> sameName //results in 0
sameFuzzy <- steve <~> sameName //results in 0

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

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:

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

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.

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

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:

Increment and decrement in EK9:

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

These operators are fairly straightforward, but the copy ':=:' operator is different in nature:

The copy principle:

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:

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:

'~' 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
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.

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

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

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:

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

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"

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:

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:

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:

  1. If the left operand is absent or unset → return the right operand
  2. Else if the right operand is absent or unset → return the left operand
  3. Else both are valid → compare them and return the appropriate one
  4. 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!

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.

//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
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.

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
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:

The generated '<=>' works through the fields:

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
<=>_cmpIntegerThree-way comparison (returns -1, 0, or 1); may be default
==_eqBooleanEquality check; may be default as the sameness, otherwise generated from <=>
<>_neqBooleanNot equals; generated from the sameness, never requested (E07181)
<_ltBooleanLess than; generated from <=> (E07181 if requested)
<=_lteBooleanLess than or equal; generated from <=> (E07181 if requested)
>_gtBooleanGreater than; generated from <=> (E07181 if requested)
>=_gteBooleanGreater than or equal; generated from <=> (E07181 if requested)
?_isSetBooleanCheck that ALL fields are set (and super.?)
$_stringString?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
$$_jsonJSONJSON object of the properties, inherited ones included (an unset property is JSON null); UNSET only if there is nothing to render
#?_hashcodeInteger?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.