Flow Control in EK9
EK9 has a range of syntax that enables a developer to control the flow of processing.
These are broadly what you would expect in a general programming language and are similar to most other programming languages.
But there are a couple of additions in EK9 that mean you may not need to use these imperative (traditional) flow control
statements quite as much. There are also a number of enhancements that make flow control much easier in EK9.
The Controls
In general it has been found that adopting a more functional approach to flow control leads to
a decrease in developer cognitive load, simplicity, flexibility, reduction in code volume, more code
reuse and fewer defects.
Linked with the new EK9 operators; traditional flow control is needed much less (but in some cases it is
still required).
The examples shown below start with simple version of each flow-control syntax and then show the additional features in EK9.
The traditional flow control mechanisms available are broadly categorised as follows:
Branching
- If, Else If ,Else - conditional processing based on a boolean expression
- Switch - conditional processing but based on variable value
- Try - see Exceptions - as the flow control is out of the current scope block (but should be 'exceptional').
Looping (imperative/procedural)
- For - traditional for loop but with extras like support for collections/iterators
- While - traditional while loop with boolean control
- Do While - traditional do/while loop with boolean control
Streaming (more functional)
Most of the above processing can also be implemented using this more functional technique.
- For - used with literals to pipe a variable into a stream
- Cat - used with a collection to pipe each element into a stream
Control examples
Examples of the different types of flow control are show below in the next sections.
If statements
A traditional control statement - but also review ternary operators and assignment coalescing. You may find that syntax much more appealing and terse in certain situations. See contrast of if/else and ternary operators for common situations and especially for dealing with variables that may be un-set.
But when you really do need an if statement EK9 has a number of additional features that give you more control and flexibility.
Simple If statements
This is the 'if/else' in its simplest form.
#!ek9 defines module introduction defines function testIfElseInteger() -> first as Integer trigger <- false if first > 21 trigger: true else if first < 10 trigger: false else trigger: Boolean() //Just ensure referenced assert trigger? testIfElseIntegerAsPure() as pure -> first as Integer trigger <- Boolean() if first > 21 trigger :=? true else if first < 10 trigger :=? false //Just ensure referenced assert trigger? //EOF
The above example testIfElseInteger should be fairly straightforward to understand, basically depending on the value of first, the value of trigger is altered.
The second example testIfElseIntegerAsPure above, highlights how the pure keyword can be used. This has a more strict use/reuse of variables and assignment. Note the use of :=? this is the assignment (but only when the 'left-hand-side' is un-set).
Range
This is a sort of if statement but with a very specific and defined use. It incorporates the EK9 concept of ranges of values. It is very useful and terse for simple situations where a developer need to check if a value is or is not within a specific range.
#!ek9 defines module introduction defines function kotlinLikeRangeCheck() -> value as Integer lowerBound as Integer upperBound as Integer stdout <- Stdout() isWithin <- value in lowerBound ... upperBound stdout.println(`${value} is within a range of ${lowerBound} ... ${upperBound}: ${isWithin}`) isNotWithin <- value not in 16-2 ... upperBound-1 stdout.println(`${value} is NOT within a range of 16-2 ... ${upperBound - 1}: ${isNotWithin}`) //EOF
The example above shows how checks can be made on a variable to check they are (or are not) within a specific
range of values. Note that the start and end values can be calculated values and also in the form of expressions.
In someways it is similar to a ternary expression or a very simple if expression (EK9 uses switch for this type of
flow control - where the conditions of more complex).
If statement with boolean logic
Simple example of using combination boolean logic as part of the if statement.
#!ek9 defines module introduction defines function currentTemperature() as pure -> country as String <- temp as Integer: Integer() if country == "GB" temp :=? 20 else if country == "DE" temp :=? 41 simpleIf() stdout <- Stdout() suitableValue <- String() valueToTest <- 9 if valueToTest < 10 stdout.println(`Value ${valueToTest} is less than 10`) secondValue <- 21 specialCondition <- true if valueToTest < 10 and secondValue > 19 or specialCondition stdout.println("Test OK") //The same logic as above - but in a different layout. if valueToTest < 10 and secondValue > 19 or specialCondition stdout.println("Test OK") //Rather than use the keyword 'if' you can use 'when' when valueToTest < 10 stdout.println(`Value ${valueToTest} is less than 10`) //As you would expect it is possible to chain if and else statements if valueToTest > 9 suitableValue: "Too High" else if valueToTest < 9 suitableValue: "Too Low" else suitableValue: "Just Right" stdout.println(suitableValue) //EOF
This example (above) shows alternative syntax and layout and combination logic expressions.
If statement with assignment/guard and declaration
In the same sort of way the switch (see later) supports guard, assignment and declarations as part of its structure, the if statement has the same features.
#!ek9 defines module introduction defines function currentTemperature() as pure -> country as String <- temp as Integer: Integer() if country == "GB" temp :=? 20 else if country == "DE" temp :=? 41 assignmentInIf() stdout <- Stdout() //Note that this value is 'unset' in the sense it has no meaningful value selectedTemperature <- Integer() //See if we can find some hot temperature somewhere so in the US we have no idea! because value is unset when selectedTemperature := currentTemperature("US") with selectedTemperature > 50 stdout.println(`Temp of ${selectedTemperature} a little warm in the US`) else when selectedTemperature := currentTemperature("US") with selectedTemperature < 50 stdout.println(`Temp of " + ${selectedTemperature} not too bad in the US`) else when selectedTemperature := currentTemperature("GB") with selectedTemperature > 40 stdout.println(`Temp of " + ${selectedTemperature} a little warm in the UK`) else when selectedTemperature := currentTemperature("DE") with selectedTemperature > 40 stdout.println(`Temp of ${selectedTemperature} a little warm in the DE`) else stdout.println("Not sure where it might be warm") guardedAssignmentInIf() stdout <- Stdout() selectedTemperature <- Integer() //Here we use a guarded assignment checks for null and unset and only then does the conditional check //Also note we can still use 'if' and rather than 'with' use 'then' when selectedTemperature ?= currentTemperature("US") with selectedTemperature > 50 stdout.println("Temp of " + $selectedTemperature + " a little warm in the US") else when selectedTemperature ?= currentTemperature("US") with selectedTemperature < 50 stdout.println("Temp of " + $selectedTemperature + " not too bad in the US") else if selectedTemperature ?= currentTemperature("GB") with selectedTemperature > 40 stdout.println("Temp of " + $selectedTemperature + " a little warm in the UK") else when selectedTemperature ?= currentTemperature("DE") then selectedTemperature > 40 stdout.println("Temp of " + $selectedTemperature + " a little warm in the DE") else stdout.println("Not sure where it might be warm") declarationInIf() stdout <- Stdout() when selectedTemperature <- currentTemperature("US") with selectedTemperature > 50 stdout.println("Temp of " + $selectedTemperature + " a little warm in the US") else when selectedTemperature ?= currentTemperature("US") with selectedTemperature < 50 stdout.println("Temp of " + $selectedTemperature + " not too bad in the US") else if selectedTemperature := currentTemperature("GB") with selectedTemperature > 40 stdout.println("Temp of " + $selectedTemperature + " a little warm in the UK") else when selectedTemperature: currentTemperature("DE") then selectedTemperature > 40 stdout.println("Temp of " + $selectedTemperature + " a little warm in the DE") else stdout.println("Not sure where it might be warm") //EOF
If statement summary
The main points in the examples of 'if statements' above are:
- The use of when rather than if in some cases - developer choice
- The use of an assignment := as part of the if statement
- The use of the guarded assignment ?= as part of the if statement
- The use of a variable declaration ← as part of the if statement
- The calling of function currentTemperature only when needed in else if
- Ideally all conditions in an if statement are on the same line
- If multiple lines for if statements are needed then they must align as above.
Note that if there are multiple if/else statements and those have declarations of variables, those variables are then available in other else/if scopes after the declaration (as part of the main if scope block). But sure you want to do this - as it may become confusing or hard to refactor later.
Being able to incorporate an assignment within the if or else if really does help to reduce the number of variables that are needed to created and assigned before the if statement block. The guarded assignment is particularly useful as the condition part of the if statement is not evaluated if the guarded assignment resulted in isSet == false.
In general, the use of the declaration/assignment or guarded assignment reads better with when rather than if, but this is developer choice. As you can see, the combination of ternary operators and the addition of assignments and guarded assignments really adds quite a lot of functionality to conditional flow control. This is augmented further in the switch statement that follows.
The guarded assignment is very useful for dealing with methods or functions that may return un-set values as they keep the additional checks much simpler.
The next section covers the switch statement/expression - which is designed to take over when there are many conditions to consider.
Switch
The switch in EK9 is similar in someways to the if/else type flow; the ordering of cases is important. It also supports multiple and varied matches and can be used to return a value like an expression. Just like the if statement it can also use alternative keywords.
When the switch is used an enumeration it works more like a traditional switch as seen in other programming languages. Indeed, it then mandates that all enumeration values are handled by a specific case (and default condition must also be provided).
Most importantly the switch statement can also be a switch expression - this means that the switch can actually return a value. Importantly there must always be a default case with the switch.
The examples below show some different uses of the switch statement/expression.
Switch on Enumerated Values
This is the most similar use of a switch statement when compared to other programming languages, but even then
it has some very strict semantics.
When used with an 'enumeration', where the case statements refer directly to
an enumerated value - all enumerated values must be defined in a case.
A default must also be provided for situations where the value being 'switched over' is un-set.
The switch is shown below in 'expression' form, but can also be used as a 'statement'.
#!ek9 defines module introduction defines type <?- This is the enumeration that is used in the example below. It is finite in the number of values it can hold. -?> LimitedEnum A, B, C, D defines function SimpleExhaustiveEnumerationExpressionSwitch() -> val as LimitedEnum result <- switch val <- rtn as String? case LimitedEnum.A rtn: "Just A" case LimitedEnum.B rtn: "Just B" case LimitedEnum.C rtn: "Just C" case LimitedEnum.D rtn: "Just D" default rtn: "Val is not set" assert result? //EOF
The example above, shows the definition of a simple 'Enumerated type', then when provided with a variable 'val' which is of the 'Enumerated type' LimitedEnum; a switch expression is used to compare that value with each of the values in the 'Enumerated type'. Note the use of the returning values from the switch expression.
When used this way switch requires all enumerated values to appear in a case and a default statement. In this way the switch is exhaustive in its semantic checking. The EK9 compiler will issue errors if not all enumerated values are present or if the default statement is missing.
You might consider this too 'strict', but it has a significant advantage in highlighting common errors that present themselves when additional values are added to the 'Enumerated type'. In this situation existing code will fail to compile when an additional enumerated value is added to the 'Enumerated type'. This is a positive situation because it now forces the developer to assess the processing.
If you do not want to deal with each enumerated value explicitly, there is a simple and elegant solution to this. This is shown in the following example.
... defines function SimpleNonExhaustiveEnumerationExpressionSwitch() -> val as LimitedEnum result <- switch val <- rtn as String? case == LimitedEnum.A rtn: "Just A" default rtn: "Val is anything but A" assert result? //EOF
By simply making one of the case statements operate as an 'expression' in this case using the == (equality) operator, the exhaustive nature of the switch statement is turned off. Now if additional enumerated values are added to the 'Enumerated type' they automatically get dealt with in the default block.
This provides a less strict switch, similar to many other programming languages.
Switch form similar to an if expression
The following example, shows how a switch expression can be employed in a similar manner to an 'if expression'.
#!ek9 defines module introduction defines function SimpleSwitchExampleWithTernaryAsAnAlternative() -> conditionVariable as Boolean //This is the reason that an ifExpression is not needed. resultValue <- given conditionVariable <- result String? when true result: "Steve" default result: "Not Steve" assert resultValue? //But above could just have been a ternary resultValue2 <- conditionVariable <- "Steve" else "Not Steve" assert resultValue2? //EOF
The above example highlights why EK9 does not have an 'if expression', the switch is so flexible and
can be used as an expression (as shown) that it provides all the functionality needed.
However, for simple expressions like this a 'ternary statement' (also shown) could have been used.
When the blocks in the 'when true' or default statements are multiline the 'ternary' would not have been usable and the switch expression with larger blocks would have been more appropriate.
Switch in statement form
The following example, shows a simple switch statement, but with expressions as part of the case statements.
The alternative syntax of given/when rather than switch/case is also shown.
This example also highlights the use of the 'pure' syntax and the necessary change in semantics to ensure variables are only assigned to when unset (this is the :=? syntax).
#!ek9 defines module introduction defines function currentTemperature() as pure -> country as String <- temp as Integer: Integer() if country == "GB" temp :=? 20 else if country == "DE" temp :=? 41 ASimpleSwitchStatement() as pure -> conditionVariable as Integer multiplier <- 5 //This is what we will vary based on the condition variable resultText1 <- String() switch conditionVariable case < 12 resultText1 :=? "Moderate" case > 10*multiplier resultText1 :=? "Very High" case 25, 26, 27 resultText1 :=? "Slightly High" case currentTemperature("GB"), 21, 22, 23, 24 resultText1 :=? "Perfect" default resultText1 :=? "Not Suitable" assert resultText1? resultText2 <- String() //The same switch could have been written using given and when given conditionVariable when < 12 resultText2 :=? "Moderate" when > 10*multiplier resultText2 :=? "Very High" when 25, 26, 27 resultText2 :=? "Slightly High" when currentTemperature("GB"), 21, 22, 23, 24 resultText2 :=? "Perfect" default resultText2 :=? "Not Suitable" assert resultText2? //EOF
As you can see rather than just matching absolute single values in the case, EK9 supports a wide range of matches. This makes it very important that you present the order correctly, as EK9 will only match the first in the list. It is also very important that when using functions in the match there are no side effects from those functions.
Unlike other programming languages there is no break or yield use. There is no case 'fall-through'; this catered for by allowing multiple expression conditions per case.
The switch in EK9 has been given great power and versatility - take care with it. Elect to do the simplest matching where ever possible.
Switch in expression form
This example shows a function in 'pure' form again. This time the switch has a return value and is used as an expression. The use of switch in expression form fits well with the 'pure' nature of only assigning a value to a variable once. It also shows some of the other case expression conditions.
#!ek9 defines module introduction defines function ASwitchAsExpression() as pure -> conditionVariable as String //This is more like a chained if/else with expressions in the case resultText <- switch conditionVariable <- result String? case 'D' result :=? "Inappropriate" case matches /[nN]ame/ result :=? "Perfect" case > "Gandalf" result :=? "Moderate" case < "Charlie" result :=? "Very High" default result :=? "Suitable" assert resultText? //EOF
In the ASwitchAsExpression example above you can see how the switch is used in an expression that both declares a new variable resultText and initialises it from the return value from the switch.
As shown above, it is possible to match String values with both lexical comparison and regular expression matches. Just to reiterate - focus on the ordering of the list in the switch statement. If you've used other languages then the switch statement tends to only match single values. EK9 introduces range matching and while this is more powerful it requires much more focus to get right.
Switch with a guard/assignment/declaration condition
The concept of a guard expression is much like a containing 'if statement'. In this example if the returning value from 'currentTemperature' was 'un-set' then the variable 'temperature' would remain 'un-set'. If that was the case then the whole switch expression would not be evaluated at all. This would mean that 'resultText' would also be 'un-set'.
The assignment and declaration form always process the switch statement, it's just the declaration is completed inside the switch scope.
#!ek9 defines module introduction defines function currentTemperature() as pure -> country as String <- temp as Integer: Integer() if country == "GB" temp :=? 20 else if country == "DE" temp :=? 41 ASwitchWithGuard() temperature <- Integer() multiplier <- 5 resultText <- switch temperature ?= currentTemperature("GB") with temperature <- result String: String() case < 12 result: "Moderate" case > 10*multiplier result: "Very High" case 25, 26, 27 result: "Slightly High" case 21, 22, 23, 24 result: "Perfect" default result: "Suitable" assert resultText? ASwitchWithAssignment() -> multiplier as Integer temperature <- Integer() resultText <- switch temperature := currentTemperature("GB") with temperature <- result String: String() case < 12 result: "Moderate" case > 10*multiplier result: "Very High" case 25, 26, 27 result: "Slightly High" case 21, 22, 23, 24 result: "Perfect" default result: "Suitable" assert resultText? ASwitchWithDeclaration() -> multiplier as Integer resultText <- switch temperature <- currentTemperature("GB") with temperature <- result String: String() case < 12 result: "Moderate" case > 10*multiplier result: "Very High" case 25, 26, 27 result: "Slightly High" case 21, 22, 23, 24 result: "Perfect" default result: "Suitable" assert resultText? //So this means the 'temperature' defined above is now out of scope and so //can be declared a-fresh and used here temperature <- "Some other value" assert temperature? //EOF
One final point, the switches in EK9 must have a default and there is no fall through (and hence no break keyword like in C/C++/C#/Java). The fall through capability is delivered by allowing multiple values per case.
A lot of capability and flexibility has been added into the switch. The EK9 switch is much more like an if/else chain than a switch from C, C++ or Java.
Try Catch and Exceptions
Whilst Try, Catch and Exceptions are a form of flow control. The flow control is one of processing an error state. It is not designed and should not be used to control normal flow. In some ways Exceptions are a form of GOTO statement (as are break and return - but in a slightly more controlled manner). In general, whilst many programming languages have a widespread use of Exceptions; these tend to pervade all API's and force alterations in interfaces. EK9 prefers the use of unSet variables, Result and the careful and selective use of Exceptions. See the section on Exceptions/Error Handling for more details.
EK9 is in general much less likely to throw an Exception and is more likely to return values that are unSet or a Result type. This means that the caller must be prepared for values that have not been set. This is the general ethos in EK9. Exceptions really should be thrown in exceptional circumstances, and never as a design mechanism for flow control.
For Loops - but with extras
Loops are normally employed when need to perform the same set of operations over some set of data.
There is quite a bit of flexibility with for loops. A key point to note that with EK9, there is no
break and no return statement; so you cannot exit for loops early (this is by design). See
EK9 philosophy for a discussion on the reasoning for this.
For loops in EK9 are intended and designed to run from end to end; all the way through, if you are looking to stop processing some way through a loop then consider using a while loop, streaming for loops or streaming collections.
Standard For Loop
The example below shows a standard traditional loop with an integer variable. There are a couple of things to note here:
- The loop variable type is inferred
- The use of '...' indicates an inclusive range
- The incrementer amount can be specified
- Its use is not limited to Integers, any type with the appropriate operators can be used
#!ek9 defines module introduction defines function ForLoopExample1 stdout <- Stdout() for i in 1 ... 10 stdout.println(`Value [${i}]`) ForLoopExample2 stdout <- Stdout() for i in 0 ... 10 by 2 stdout.println(`Value [${i}]`) ForLoopExample3 stdout <- Stdout() j <- 2 for i in 0 ... 10 by j stdout.println(`Value [${i}]`) ForLoopExample4 stdout <- Stdout() j <- -2 for i in 10 ... 0 by j stdout.println(`Value [${i}]`) ForLoopAsExpression() result <- for i in 1 ... 10 <- rtn <- 0 rtn += i assert result? //EOF
Please note the spaces around '...'; these are required.
Importantly it is also possible to use a for loop in the form of an expression that returns a value. This is shown above in function 'ForLoopAsExpression'.
For Loop with other types
In the above example an Integer was used, these examples below show the for loop has much more flexibility. Indeed, it can be used with any types (even those created by EK9 developers) as long as the appropriate comparison/addition/subtraction operators are provided.
#!ek9 defines module introduction defines function FloatForLoop() stdout <- Stdout() //So you could do a calculation to get this value incrementer <- 6.3 for i in 8.2 ... 30.0 by incrementer stdout.println(`Value [${i}]`) //descending for i in 90.0 ... 70.0 by -5.0 stdout.println(`Value [${i}]`) TimeForLoop() stdout <- Stdout() //From 9-5:30 every half hour start <- Time().startOfDay() + PT9H end <- Time().endOfDay() - PT6H30M thirtyMinutes <- PT30M //Be aware that time loops around. for i in start ... end by thirtyMinutes stdout.println(`Value [${i}]`) //EOF
Hopefully now you can see the additional power and flexibility the EK9 for loop has.
- It can be used with different types
- It can be used ascending or descending
- The loop variable type is inferred and is immutable (developers cannot alter its value directly)
- Variables can be used for the start and end of the range and also the incrementer
- By providing your own constructs with the right operators enables them to be used in 'for' loops
- It can be used as an expression, returning a value
For Loop with collections
Unlike some other Object-Oriented languages; EK9 does not attach for loop syntax to Collection
objects, nor does it attach stream syntax.
It approaches the traversal of objects held within Collections in two different,
but complementary ways. The first of which is shown below and the second is shown in
streaming collections
#!ek9 defines module introduction defines function CollectionsForLoop() stdout <- Stdout() stdout.println("Strings") for item in ["Alpha", "Beta", "Charlie"] stdout.println(item) stdout.println("Characters") //The alternative is when you already have an iterator moreItems <- ['A', 'B', 'C'] for item in moreItems.iterator() stdout.println(item) //EOF
The loop variable's type is inferred from the Collection or Iterator. The for loop on Collections can be used with any type that has an iterator() method that returns an Iterator or with an object with type that has two methods below (which is a bit of a 'Duck Type' approach):
- hasNext() - returning a Boolean
- next() - returning a type other than Void
The example above shows a List of String being used in a for loop and also the Iterator from a List of Characters.
For Loop assignments, guards and declarations
In the same way the 'assignments', 'guards' and 'declarations' can be used with if and switch statements/expressions, they can also be used with for loops. This is shown below.
#!ek9 defines module introduction defines function messagePrefix() <- rtn <- "SomePrefix-" forLoopWithGuard() stdout <- Stdout() prefix as String? for prefix ?= messagePrefix() then i in 1 ... 10 stdout.println(`${prefix}${i}`) forLoopWithAssignment() stdout <- Stdout() prefix as String? for prefix: messagePrefix() then i in 1 ... 10 stdout.println(`${prefix}${i}`) forLoopWithDeclaration() stdout <- Stdout() for prefix <- messagePrefix() then i in 1 ... 10 stdout.println(`${prefix}${i}`) //Just to highlight that 'prefix' in the for loop - was scoped in the for loop only. prefix <- 21 assert prefix? //EOF
The capability to add variable initialisation as part of the for loop makes is consistent with most of the other EK9 flow control syntax.
Streaming 'For'
There are times when the values generated by a for loop are useful in a processing pipeline. The example below shows this with the addition of some filtering.
#!ek9 defines module introduction workHours() as pure -> time as Time <- workTime as Boolean: time < 12:00 or time > 14:00 defines program TimePipeLine() stdout <- Stdout() //From 9-5:30 every half hour start <- Time().startOfDay() + PT9H end <- Time().endOfDay() - PT6H30M thirtyMinutes <- PT30M for i in start ... end by thirtyMinutes | filter by workHours > stdout //EOF
In the example above, the for loop is used to generate a sequence of times and these are then 'piped' into a processing pipeline and processed by the function 'workHours'. The pipeline then looks at the result of the filter and only outputs the time to the next stage of processing if 'true'. Hence, 'stdout' only gets the times that are before 12:00 and after 14:00.
Hopefully from this example you can see how the omission of break and return doesn't really present a major issue and how you can un-bundle nested loops (always tricky) into processing pipelines. But it also means that the bit of logic you might have nested in the for loop has been pulled out to a function (that can be tested, re-used and altered with ease).
This different syntax and approach does take a little time to get used to. If you notice, where an if statement to determine the work hours, this has been implemented with a simple boolean expression (predicate) in the function 'workHours'. While you may think this a trivial point - after all you still had to write the same boolean expression. You also had to write the function which is much more text than 'if'. The overall approach has moved from being procedural to being more declarative.
By using head with the pipeline processing you can cut short the streaming of objects through the pipeline. As soon as head has process the number of objects you requested it will trigger the shutdown of the pipeline. This is very much like break and return. So if; for example you were looking for a particular value in a collection or a generated list, you can use a filter to select the right one; then head to stop processing once one value with that criteria comes through the pipeline. Clearly it may be the case that the value never comes through the pipeline (if the value was not contained).
One final point; the Streaming 'For' is the main reason there are no List Comprehensions in EK9. While the syntax in EK9 and the need for functions means more code is required (than List Comprehensions in Python), it makes the code more readable and fits the EK9 philosophy.
This approach also brings standardisation to functional processing flow. So unlike other languages (such as Java) where specific methods are attached to specific 'Functional types', EK9 has the functional processing syntax built in to the language itself.
Streaming Collections
As with the for loop above and the streaming for it is possible to send all the contents of collections through a processing pipeline rather than just looping over them.
#!ek9 defines module introduction defines program collectionsPipeLine() stdout <- Stdout() items <- ["Alpha", "Beta", "Charlie"] cat items > stdout //The alternative is when you already have an iterator moreItems <- ['A', 'B', 'C'] iter <- moreItems.iterator() cat iter > stdout //An example of 'teeing' content out of a processing stream capturedValues <- List() of String cat ["Alpha", "Beta", "Charlie"], ["Delta", "Echo"] | tee in capturedValues > stdout assert capturedValues? //EOF
By introducing the cat command syntax, EK9 has removed the need to attach for/stream syntax to collection classes themselves. This gives much more flexibility and consistency in developed code. In effect the developer just 'streams' the contents of a collection (or some sort of source, like an iterator) into a processing pipeline.
In the example above there is no real processing in the pipeline, the contents are just written to
stdout.
But part of the example above shows two 'lists of Strings' being streamed, tee off into a collection to be used
later and also streamed to stdout.
There is much more detail on pipeline processing in section Streams/Pipelines.
While and Do/While loops
The final two loop flow control mechanisms are traditional in approach. They both work on
the value of a conditional boolean check. But they too, accommodate 'assignment', 'guard' and 'declarations'.
They too can also be used as expressions and can return values.
While loop examples
An example of a while loop, iterating over a collection of Characters, this includes a demonstration of the use of 'guard', 'assignment' and 'declaration syntax as well.
The final function 'whileLoopAsExpression' shows the while loop being used as an expression and in conjunction with a variable 'declaration'.
#!ek9 defines module introduction defines function collectionsWhileLoop() stdout <- Stdout() moreItems <- ['A', 'B', 'C'] stdout.println("Characters while") itemIter <- moreItems.iterator() while itemIter.hasNext() item <- itemIter.next() stdout.println(item) //Now a while with a guard, so reuse the iter from above but reset while itemIter ?= moreItems.iterator() then itemIter.hasNext() item <- itemIter.next() stdout.println(item) //Same again but with an assignment while itemIter := moreItems.iterator() then itemIter.hasNext() item <- itemIter.next() stdout.println(item) //This time declare a new iterator, but within the while loop while itemIterX <- moreItems.iterator() then itemIterX.hasNext() item <- itemIterX.next() stdout.println(item) whileLoopAsExpression() result <- while complete <- false with not complete <- rtn <- 0 rtn++ complete: rtn == 10 assert result? //EOF
Do/While loop example
This processing is similar to the above except the conditional logic is at the end of the processing block.
#!ek9 defines module introduction defines function collectionsDoWhileLoop() stdout <- Stdout() moreItems <- ['A', 'B', 'C'] itemIter <- moreItems.iterator() if itemIter.hasNext() do item <- itemIter.next() stdout.println(item) while itemIter.hasNext() //The above is a bit clumsy, so lets try this instead //This has the same effect, when the iterator is unset do itemIter ?= moreItems.iterator() then item <- itemIter.next() stdout.println(item) while itemIter.hasNext() doLoopAsExpression() result <- do complete <- false <- rtn <- 0 rtn++ complete: rtn == 10 while not complete assert result? //Just to show that 'complete' is bound into the do/while scope complete <- "Almost" assert complete? //EOF
As can be seen from the examples above the 'guard' expression part when used with a do/while statement/expression makes the processing block slightly simpler. The do/while expression is also a common sort of programming 'pattern' and again the 'declaration' in the do/while makes this much more self contained.
Summary
EK9 has a wide range of flow control features, but it has deliberately excluded some of the more traditional syntax (like break for example). This is by design, as it is viewed that some of these elements lead to complexity and defects (a bit like GOTO).
However, EK9 has added new syntax to traditional flow control syntax, this mainly being the introduction of:
- Guard statements
- Assignment statements
- Declaration statements
- Switch, For, While and Do/While in expression form
The use of ternary operators and assignment coalescing should be adopted in place of very simple if/else statements; they are short/succinct and expressive. However, where a range of different methods or functions need evaluation the guarded assignment in conjunction with the if statement can reduce the amount of processing.
Finally if you find you need to chain many if else if statements together then the use of a
switch statement might be appropriate. Beyond the switch it is sometimes best to adopt a
Dictionary/Map to solve the problem.
While this may seem strange, remember that you can associate a function delegate with a key
as part of the dictionary. This means it is possible to lookup a function in a dictionary and then call that function.
It is this latter concept, that if you really do have unique values to switch on, then use a Dictionary/Map - you will probably find it is faster and easier to understand. The use of switch is designed to make larger chained if/else blocks easier to understand.
Next Steps
The next section on Exceptions and try blocks show how error flow control can be implemented.