EK9 Operators

The section on basics details how to declare variables, add comments and define visibility of functions, methods and fields. It also covers line termination and indentation (code blocks).

This section lists all the operators that EK9 supports and details their meaning. There are quite a few examples on both how to use the operators; but also how to override operators in both records and classes. The operators have been grouped to help explain related ideas. In some cases the operator is used in a slightly different way, depending on the types. Other languages may use different operators, so take care. Specifically and/or has meaning as both in terms of a boolean operator but also in terms of a bitwise operator.

The Operators

There are quite a few operators, most of which will be obvious, but there are a few new ones. EK9 make heavy use of operators as it improves readability and consistency of code.

Types of Operator

There are three broad types of operator, these are:

A quick note on ternary operators, some developers find these expressions a little confusing and resort to if/else statements, EK9 does have some shortcut syntax that is worth reviewing as it may help. The ternary statement above can be read as:
"If a == true then set result to b else set result to c"

What are Operators

Before we get started on each of the groups/types of operator and their specifics; it is worth understanding what we mean by an operator.

The operator is really a shorthand and consistent way of defining a method on an Object - this method can be a symbol or a phrase. The main difference is that the operator looks disconnected from the object itself.

This expression result := a + b could be read as result := a.+(b) but the former is much more natural and clearer (unless you like Lisp; then (+ a b) would be clearer for you).

As an example, the concept of addition will be familiar to everyone and is normally expressed in mathematical terms as the symbol '+'. Well pretty obvious you may think but there is something that has been misused or confused in many programming languages.

In the expression below neither 6 nor 8 is modified.

  • result 6 + 8

This lack of modification is as you would expect in mathematical terms. But some software API's use (misuse) a method called add to add something to a value. So the semantics of addition have been altered. An example of this would be Java List add.

The intention here is not to denigrate Java or any other language, but to highlight the value of operators and the retention of semantics (meaning) in conjunction with most peoples long-term memory.

To accomplish adding something to a value in EK9 you would use the '+=' operator. If you used the '+' then a new List would be provided with the contents of the existing List plus the value. See the example below.

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

This discussion around the semantics of add might seem petty or trivial, but it is absolutely critical when developing generic functions and classes as '+' really must have the same meaning irrespective of type. It is also important in terms of readability, most people are familiar with basic mathematics and so it is very important to retain the semantics of '+' (add).

Being able to read code quickly and immediately see what it accomplishes is very valuable.

The example above using the '+' operator with a List and a String, but also with two Lists highlights this point. In both cases it is obvious that the addition of two items will result in a third item containing the contents of both, but the two items being added will remain unaltered.

It is also important not to introduce new and possibly misleading methods like 'addTo', 'addInTo', 'addAll', 'join', 'joinTogether'. When existing operators like '+' and '+=' can be defined and overridden/overloaded with different parameters, but with the same semantics.

There's been quite a bit of thought and research put into this. Employing a developer's long term memory for really fundamental concepts and semantic meaning that common. Employing symbols and ideograms to leverage the visual side of the developers brain and reducing cognitive load.

Focus

It is very easy to become seduced by operators, to override/overload and twist them into contrived meanings. When overriding an operator it is really worth taking the time to ensure it retains its semantics and consistency. But probably more importantly; the simple use of the '+' operator really could have triggered quite a lot of processing. In the case of two Lists it may have triggered the creation of a new List and the full traversal of both of the other two Lists to populate the return list. This amount of processing may not be obvious from just using the '+' operator!

Or the processing might have created a list with just references to the two lists, giving the impression of joining them. The implementation detail is not important here, but the semantics are.

Operators have been created to be used and overridden. If you are creating an API that uses imaginary numbers and you want to express addition then overriding the '+' is not only acceptable it is strongly encouraged. If you are creating some type of collection again using '+' and '+=' to 'add' or 'add to' is strongly encouraged.

Use them in the manner that was intended and when you need to employ your types with generics the solution will be much simpler, if you are from a C++ background this will feel natural - but take care not to misuse operator overriding/overloading.

The Operators themselves

The first group of operators to be described are those that are used for comparison. Most of which will be obvious, but there are a couple that need closer attention (such as the fuzzy match).

Comparison Operators

The first set of operators to be explained are those used in comparisons and checks and return a Boolean value.

Symbolic Boolean Operators

Typically used in a if statement the following operators are used to compare the value of two objects and the result will be a Boolean value of true or false (or not set if either a or b are not set).

The last point above is important, if operands are unset then the result will be unset.

Examples of the comparison operators

  • 1 < 2 //results in true
  • 1 > 2 //results in false
  • 3 >= 2 //results in true
  • 3 <= 2 //results in false
  • 3 <> 3 //results in false
  • 3 != 3 //results in false
  • 3 == 3 //results in true
  • 3 in 2 ... 8 //results in true
  • 6 not in 2 ... 8 //results in false
Comparison Phrase Operators

The following two operators are full words (phrases) rather than symbols that return a Boolean, these are:

Examples of use are:

  • "Steve" contains "Te" //results in false
  • "Steve" contains "te" //results in true
  • "Steve" matches /[S|s]te(?:ven?|phen)/ //results in true
  • /[S|s]te(?:ven?|phen)/ matches "Steve" //also results in true
Symbolic Comparison Operators

The following two operators use symbols which you may not have seen before; they return an Integer value, these are:

Examples of use are:

  • "Steve" <=> "Steve" //results in 0
  • "Steve" <~> "Steve" //results in 0

While you may think these two operators appear to do the same thing, they are in fact very different in character. The comparison operator '<=>' is intended to do a simple compare and in the case of Strings this would be lexical. For your own records and classes you can implement your own comparison mechanism; but keep to the same semantics.

The fuzzy match operator '<~>' is more sophisticated and is a sort of bridge between the comparison operator and regular expressions. Below are a couple of examples and while these examples are given as Strings you can implement your own fuzzy match on your own records and classes.

  • "Steve" <=> "steve" //results in 1
  • "Steve" <~> "Steven" //results in 2
  • "Steve" <~> "teve" //results in 1
  • "Steve" <~> "steven" //results in 3

The idea behind the fuzzy match is to try and find the best sort of match for a particular value. In this case Strings and specifically steve is the best match because just the case of the 's' had to be altered. The String built into EK9 uses a modified Levenshtein algorithm, that weights case changes, character additions and spans of common sequences slightly differently.

For your own aggregate types you could implement any sort of scoring mechanism, this is useful for sparse or incomplete data and especially when accepting user input where the validity or addresses or names could be matched against a range of known valid entries. For example; you would probably weight address fuzzy matching on different levels for country, city, street and house number. A single change of a digit in a house number has less weight than the country being "US" or "UK" for example. For some fuzzy matching you might use the proximity of keys on the keyboard being next to each other to accept 'typos' and find the actual logic word the user meant.

The mechanisms you use for your own fuzzy match should however retain the meaning of 'fuzzy'.

By creating a fuzzy match on your own developed types you will be able to use and reuse a range of generic functions and classes should they use fuzzy match in their implementation. You could imagine a sorting function that employed the fuzzy comparison to order items, if this were a generic function it could just be re-used with your types if you implemented the fuzzy match operator.

Symbolic Unary Operator

All of the operators shown so far are binary operators; the next operator is a postfix unary operator and is very useful for checking the validity of any value.

Examples of use are:

  • name1 "Bill"
  • name2 "Ted"
  • name1? //results in true
  •  
  • name3 as String
  • name3? //results in false
  •  
  • name4 String()
  • 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.
  •  
  • (name1 < name2)?
  • //results in true - because the calculation was valid not because Bill is Less than Ted
  • (name1 < name3)?
  • //results in false - because the calculation was invalid name is not set

The is set operator should always be present in your own implementation of records and classes as it provides a very clear and consistent way to establish whether an object contains valid data or not. It means you can create new instances of Objects without the need to provide any valid data (you may not know the values yet).

For example; lets say an Address is an aggregate of several data elements, house number, street, city and country. If you are to ask a user for such information then an Address Object needs to be created to hold it, but initially it will not be valid. Only when the user has provided the information can the Address be checked and only then is it valid (or not if the information is erroneous).

You may wish to consider using records to gather sets of data and then using those as arguments into classes, where the classes have both data and behaviour. The classes can then validate the records during consttruction. This reduces the number of fields in the class and enables the class to be either fully valid or fully invalid. This more simple approach reduces the number of arguments to a class and removes the need for 'Builders'.

Assignment Operator(s)

This next set of operators are only used in assignment - all three do exactly the same thing and really are just different syntax that is used to support developers from different backgrounds and also enable assignment in different contexts to look or feel more natural.

There is also a conditional assignment operator (a :=? b). This operator is different to the three above. It is like a combination of an 'if statement' and an assignment.

The assignment only take place if 'a' has no memory allocation or it is unset. Only if this is the case will the variable 'a' be assigned to point to the same data as 'b'.

The basics section discusses the reasoning for the three different assignment styles; and they do tend to feel right in different contexts (this is down to personal preference).

It is important to note that really it is only a handle (pointer) that is assigned and not actual data. So using the example of the address previously.

As there are no primitives in EK9 even for Integer and Float; it is worth stating again if you alter the value that two or more variables reference; all references will see that change. This is not what you will be expecting if you are used to languages that use primitives and not Objects.

  • checkAddr as Address //not assigned to anything
  • 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
  • checkArr.country = "US"
  • //myAddress.country is now also "US"
  •  
  • anAddress as Address //not assigned to anything
  •  
  • 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 four different types of assignment operator have been shown in the example above.

For any developer with a background of C#/Java this will feel normal and natural. In effect the variables of myAddress and checkAddr are just pointers to a bit of memory that holds the actual Address data. They both point to the same bit of memory for the Address. There are times when you do want a full copy or clone of the data, for that see the next section on modification operators specifically the ':=:' (copy) and ':~:' (merge) operators.

There are also times when it is necessary to 'replace' an entry in a collection of some sort, the ':^:' (replace) operator provides a shorthand and standardised naming semantics.

The conditional assignment operator (coalescing assignment) is new however and is a nice shorthand for simple if statements followed by assignment.

Modification/Mutation Operators

To be able to alter the contents of a variable (not just the references to the memory location of the variable), there are number of operators that can be used.

Note: the increment and decrement operators are not prefix/postfix type operations. They just increment and decrement, unlike operations on primitives in C,C++,C# and Java.

Here are a number of examples of the above operators to illustrate their functionality.

  • 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 as 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 as 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

The operators above are comparatively simple and are fairly straight forward - but check the copy ':=:' operator as that is quite different in nature.

The merge ':~:' operator is similar in many ways to copy ':=:' operator in the fact that it alters the underlying content of the variable. In general the merge ':~:' operator is very useful when used with aggregates. Below is an example of a record; this treats fields that are not set as being suitable for replacement. There is quite a bit going on in this example, it shows a couple of other operators that have not yet need described, but is more fully featured as an example.

#!ek9
defines module introduction

  defines record
  
    Person
      first <- String()
      last <- String()
      dob <- Date()
      profession <- String()
      
      //is set
      operator ? as pure
        <- rtn Boolean: first? and last? and dob? and profession?
      
      //convert to string
      operator $ as pure
        <- rtn String: first + ", " + last + ", " + $dob + ", " + profession
      
      //equality
      operator == as pure
        -> arg as Person
        <- rtn as Boolean: arg? and first==arg.first and last==arg.last and dob==arg.dob and profession==arg.profession
      
      //copy
      operator :=:
        -> arg as Person
        <- rtn as Person: this

        if arg?
          first :=: arg.first
          last :=: arg.last
          dob :=: arg.dob
          profession :=: arg.profession
      
      //merge
      operator :~:
        -> arg as Person
        <- rtn as Person: this

        if arg?
          if not first?
            first :=: String(arg.first)

          //Could be written like this with coalescing assignment.

          last :=? String(arg.last)
          dob :=? Date(arg.dob)
          profession :=? String(arg.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
      smithj <- Person()
      
      smithj :~: john
      smithj :~: smith
      
      //Alter first name
      john.first: "Jon"
      
      //Just show the values.
      stdout.println("[" + $john + "]")
      stdout.println("[" + $smith + "]")
      stdout.println("[" + $smithj + "]")
//EOF          

The output would be as follows:

[Jon, , 2020-11-03, IT]
[, Smith, 1960-01-01, ]
[John, Smith, 1960-01-01, IT]

The example above is quite long; but hopefully gives you a much better idea of the operators available and how powerful they can be. Note that because the merge was done before the first name was altered to "Jon" a full deep copy of the value was made.

Boolean (Logical) and Bitwise Operators

This section focuses on a number of operators that are used by two very different types, but the concept and meaning of the operator is broadly the same. Other languages model these operators in different ways. In EK9 the same syntax is used in both contexts.

Boolean (Logical) Operators

The first set of operators are the Logical operators that are used with Boolean values.

Most of the above operators will be familiar to developers, the '~' and 'xor' may not. The '~' is the same as the 'not' operator. But not that many languages support the 'xor' operator (though it can be quickly implemented with combinations of 'and'/'or').

  • //Simple boolean values
  • a true
  • b true
  • c false
  •  
  • //Each of the operators
  • //The and/not operator
  • result a and b //result would be true
  • result a and c //result would be false
  • result a and not c //result would be true
  • result a and ~c //result would be true
  •  
  • //The or/not operator
  • result a or b //result would be true
  • result a or c //result would be true
  • result not a or c //result would be false
  • result ~a or c //result would be false
  •  
  • //The xor/not operator
  • result a xor b //result would be false
  • result a xor not b //result would be true
  • result a xor c //result would be true
  • result not a xor c //result would be false
  • result ~a xor c //result would be false
Bitwise Operators

The second set of operators are the bitwise operators that are used with bit(s) values. The look and syntax is the same as the logic operators but there are a couple of additional operators (shift left/right). But critically these work with sets of bits (these are not ints or longs - but are an ordered collection of bits).

Note that the least significant bit is on the right and the most significant bit is on the left as you would expect.

  • //Simple bits values
  • a 0b010011
  • b 0b101010
  • c 0B01010011
  •  
  • //Each of the operators
  • //The not operator
  • result not a //result would be 0b101100
  •  
  • //The and operator
  • result a and b //result would be 0b000010
  •  
  • //The or operator
  • result a or b //result would be 0b111011
  •  
  • //The xor operator
  • result a xor b //result would be 0b111001
  •  
  • //The << operator
  • result c << 1 //result would be 0b010100110
  • result c << 2 //result would be 0b0101001100
  •  
  • //The >> operator
  • result c >> 2 //result would be 0b00010100

The most important point about the bitwise operators is that they operate on sets of bits; these are not longs or ints or any other primitive type. They are only sets of bits that can grow (as you see in the shift left operator). There are more details on the set of bits type in built-in types and Standard Types. For example; it is possible to join sets of bits and use these with pipeline processing to check specific values in a sets of bits; but you cannot consider them a primitive type nor an array of bits (there are no arrays).

Ternary style Operators

There are a number of ternary operators that have a shorthand, some of which are new. See the example at the end of the section on why these were introduced and what the benefits of using them are.

The main ternary operator is shown below first as it will be familiar to many developers:

  • //Setup values
  • bird1 as String
  • bird2 "Duck"
  •  
  • birdA bird1? bird1 : bird2
  • //birdA would have the value of "Duck" because 'bird1' is not set
  • //Note this could have been written as
  • birdA := bird1? bird1 else bird2
  • //Maybe this is less terse but more readable and obvious.
  •  
  • bird1 := String()
  • birdA := bird1? bird1 : bird2
  • //birdA would still have the value of "Duck" because 'bird1' does not have a meaningful value
  •  
  • bird1 := "Goose"
  • birdB bird1? bird1 : bird2
  • //birdB would now have the value of "Goose" because bird1 is set

This is a standard ternary operation; instead of 'bird1?' any Boolean expression can be used. As you can see using 'else' rather than the ':' makes the ternary operator a little more obvious and readable.

Coalescing Operators

There are a number of coalescing operators, these work on variables that are not set. These are:

The 'Elvis' operator is a bit special (as you would expect). The Elvis operator does both a memory allocation check and an is set check. This is the main difference to the '??' operator which only does a memory allocation check.

While not a ternary operator; the conditional assignment (:=?) operator 'feels' related. This is similar in some ways to the 'Elvis' operator. This can also be considered to be an assignment coalescing operator. The right-hand side of the assignment is only evaluated if the left-hand side is unset (or has not been allocated storage).

  • //Setup values
  • bird1 as String
  •  
  • bird1 :=? "Goose"
  • //bird1 would have the value of "Goose" because 'bird1' is not set
  •  
  • //In contrast.
  • bird1 "Duck"
  •  
  • bird1 :=? "Goose"
  • //bird1 would have the value of "Duck" because 'bird1' was originally set

You maybe thinking why not just use the normal if statements and comparison operators, why add these as well. The normal comparison operators will return an unset value if either of the items being compared are unset. A value with compared to another unset value will result in a meaningless result. These operators accept that either (or both) of the values can be unset.

  • //Initialise values
  • bird1 as String
  • bird2 "Duck"
  • birdA bird1 ?? bird2
  • birdA := bird1 <? bird2
  • birdA := bird1 <=? bird2
  • birdA := bird1 >? bird2
  • birdA := bird1 >=? bird2
  • //In all above examples birdA 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.
  •  
  • birdB bird1 ?: bird2
  • //birdB would also have the value of "Duck" because 'bird1' is not set
  •  
  • bird1 := String()
  • birdA := bird1 ?? bird2
  • //birdA would now be un set and not have a meaningful value because it has taken same value as 'bird1'
  • //But importantly bird1 does have memory allocated.
  •  
  • birdB := bird1 ?: bird2
  • //birdB would have the value of "Duck" because 'bird1' does not have a meaningful value but does have memory allocated.
  • //this is the main difference between ?? and ?:
  • //By the same logic birdB would have the value of "Duck" when the <?, <=?, >?, >=? operators are used.
  •  
  • bird1 := "Goose"
  • birdC bird1 ?? bird2
  • //birdC would now have the value of "Goose" because bird1 is set
  • birdC := bird1 ?: bird2
  • //birdC would also have the value of "Goose" because bird1 is set
  • birdC := bird1 <? bird2
  • //birdC would have the value of "Duck"
  • birdC := bird1 <=? bird2
  • //birdC would have the value of "Duck"
  • birdC := bird1 >? bird2
  • //birdC would have the value of "Goose"
  • birdC := bird1 >=? bird2
  • //birdC would have the value of "Goose"

As you can see there is a small but important difference between '??' and '?:'. These operators have been introduced to make the processing of variables that have or have not been given a meaningful value easier. They simplify comparisons where some values may not have been set.

The operators '<?', '<=?', '>?' and '>=?' follow the same logic as the Elvis operator '?:' (as does the ':=?' operator). They work with variables that have no memory allocated (null) or do have memory allocated but are unset, or do have a meaningful value.

Assignment coalescing Operator

So in short; if you want to make an assignment by comparing two variables and take into account that they may not have a meaningful value use '<?', '<=?', '>?' and '>=?' in preference to '<', '<=', '>' and '>=', unless you actually need to retain the fact that the comparison was a meaningless one. If you want only want to assign a variable if it currently is unassigned then use the assignment coalescing operator (':=?').

One of the most important concepts in EK9 is that a variable can exist but not hold a meaningful value. This is really important as most developers would expect the comparison of two values to result in either:

But there is a forth state where either one or both of the values is not set. With standard comparison operators the test becomes meaningless and not valid. These operators support the idea that you may wish to compare values and account for the fact that one or both might not be set. If both do have meaning then the normal comparison operator logic is applied.

These new operators may seem like just yet another thing to learn, but you could just write the following:

  • //Values to work with
  • bird1 as String
  • bird2 "Duck"
  •  
  • //Without using new operators
  • birdA String()
  • if not bird1?
  •   birdA := bird2
  • else if not bird2?
  •   birdA := bird1
  • else
  •   birdA := bird1 < bird2 bird1 else bird2
  •  
  • //With the new operator
  • birdA bird1 <? bird2
  •  
  • //With the new operator and function calls
  • birdA functionToGetBird1() <? functionToGetBird2()
  • //Without the new operators, you would want to avoid multiple calls!

So these new operators are more than just a null safe version of the normal comparators, they are a mix of the Elvis coalescing operator and comparators/assignment operators. They really reduce the amount of null and if/else code. They also dovetail in with type inference, note how in the example above birdA is declared, type inferred and assigned all in one go.

Finally (and this section has been long) imagine if the bird1 and bird2 had not actually been variables, but instead had been calls to functions or methods. You would now be forced to use additional variables to capture the results of those calls. With the new coalescing operators that is not necessary.

Mathematical

While these sets of operators are under the section called 'Mathematical' operators; addition '+' and subtraction '-' can and should be used with other types, records and classes. A good example of this would be a collection like a List using '+', '+=', '-', '-=' make sense as it is rational, logical and is semantically correct.

Examples of these mathematical operators are shown below.

  • //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 := charlie 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 includes a number of operators that are not found in most other languages. Again these are aimed at encouraging standard naming and also readability. They also model some standard type concepts, like conversion to a String/Hash code and promotion from one type to another (i.e Integer to Float). Another concept that is used quite widely is closing of 'things', such as connections, files, collections or anything where explicit completion of use needs to be de-marked in some way. As you will see in the section on Exceptions and Error Handling the use of close with a try block means that resources that have to be managed (opened/closed) can be dealt with in quite a terse and simple manner that is consistent.

EK9 also includes an operator to convert built-in type and records/classes to JSON. In the case of records the implementation can be defaulted in most cases. But there are times when it is necessary for you to provide your own implementation.

You may have noticed there are some operators that use some additional wording like is or of these are just a bit of sugar you can omit them if you wish; but sometimes it makes the syntax 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 12
  •  
  • //Accessing specific items in the List
  • //For the first (prefix) and the last (suffix) can be used
  • v1 #< aList //v1 will be "V1"
  • v3 #> aList //v3 will be "V3"
  •  
  • //Access can be by index value 0 based
  • maybeAString aList.get(5) //maybeAString will be of type Optional of String
  • //The Optional is returned so that if invalid index is passed in a value that is not set can be provided
  • //In this case maybeAString? would be false there are only 3 items in the lit
  •  
  • //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 but with different types String rather than List
  • //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

Finally there is one operator that is only used in conjunction with Streams and Pipeline processing and that is the pipe '|' operator. It is an operator that is provided on the built-in types where appropriate (List/Integer/Float for example) to allow for the accumulation of values. But note it can also be overridden in your own Records and Classes.

Its semantics are similar to the '+=' operator but with one very significant difference. Whereas the '+=' operator typically does not allow an addition to the value when it itself is unset the '|' operator does. This means that value can transition from being unset to having an initial value when the '|' operator is first called. Subsequent calls to the '|' operator should use the value being passed in to accumulate or replace the current value (depending on the type).

See Integer type in a pipeline for an example of this.

Defaults

You may be used to your programming language or tool chain (like Java Lombok) providing many of these operators out of the box; this is so you don't have to implement them yourself.

EK9 does provide this functionality; but it does so in a layered and controlled manner that applies to records and classes only. See the sections on Records and Classes for more details.

Simple operators like '$', '#?', '?' can be generated by EK9. It uses the fields/properties held in the record/class and those of any super record/class. Those fields must also have the operators present (else the compiler will issue an error).

For records; EK9 can also automatically generate the JSON ($$) operator code by default in most cases.

The converse is not always true however, EK9 cannot always create the code to take JSON and convert back into records. If the records contain fields/properties that are abstract in nature then it falls to the developer to write that code to reconstitute a record from a JSON representation.

The Comparison/Equality operators like '==', '<>', '<=>', '<', '<=', '>', >=' can be generated by EK9 by looking at each of the fields. This is done primarily through the comparison ('<=>') operator. If the record/class has a super; this is called first; and only if results in 0 for comparison are the fields in the current instance evaluated. But if any of the fields of the record/class are unset then the result is also unset. To alter this behaviour, just override the '<=>' operator and provide the implementation you would prefer.

The idea of 'default operator' is very useful and enables you to provide all the operators in a simple and straightforward manner. But it also enables you to provide a finite list if you wish and provide your own implementations as and when you see fit.

Summary

As you can see there are quite a few operators, most should be fairly obvious; especially the logical and mathematical operators. The others are there to enable you to deal with common operations such as type promotion, hashing and string conversion in a terse/succinct manner.

The other main operators are employed to deal with variables which may or may not be set. As mentioned earlier a variable without meaningful data is an important concept and this is why the additional operators have been created.

These null safe ternary and assignment coalescing operators may take some getting used to. Just remember if you are checking if a value is being set with an 'if statement'; there maybe a built-in operator that can help reduce your code.

Next Steps

There is a bit more detail on operators in the section on built-in-types. That should be the section you review next, as structure covered the overall layout and constructs and this section covered what type of operators are available.