Flow Control in EK9

EK9 has a range of syntax that enables a developer to control the flow of processing. These are what you would expect in a programming language. But there are a couple of additions in EK9 that mean you may not need to use these imperative-traditional flow control statements as much.

The Controls

In general it has been found that adopting a more functional approach to flow control leads to a reduction in overall code volume, more simplicity, more flexibility, much more 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 flow control mechanisms available are categorised as follows:

Branching
Looping (imperative/procedural)
Streaming (more functional)

If, Else If ,Else

A traditional control statement - but review ternary operators and assignment coalescing first; as 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 control and flexibility. These are shown below.

#!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

  defines program
    simpleIf()
      stdout <- Stdout()

      suitableValue <- String()
      valueToTest <- 9

      if valueToTest < 10
        stdout.println("Value " + $valueToTest + " is less than 10")

      //Rather than use the keyword 'if' you can use 'when'
      when valueToTest < 10
        stdout.println("Value " + $valueToTest + " is less than 10")

      secondValue <- 21
      specialCondition <- true

      //Ideally all on one line
      if valueToTest < 10 and secondValue > 19 or specialCondition
        stdout.println("Test OK")

      //If they must span multiple lines then they must align like this
      if valueToTest < 10 and
      secondValue > 19 or
      specialCondition
        stdout.println("Test OK")

      //as you would expect
      if valueToTest > 9
        suitableValue: "Too High"
      else if valueToTest < 9
        suitableValue: "Too Low"
      else
        suitableValue: "Just Right"

      stdout.println(suitableValue)

    assignmentInIf()
      stdout <- Stdout()

      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")
//EOF

The main points in the example above are:

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 null or more likely isSet == false.

In general the use of the 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.

Switch

The switch in EK9 supports switch type flow; but the ordering of cases is important. It also supports multiple and varied matches and can also be used to return as value like an expression. Like the if statement it can also use alternative key words.

#!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

  defines program
    simpleSwitch()

      stdout <- Stdout()
      multiplier <- 5
      //This is what we will use to 'switch on'
      conditionVariable <- 21
      //This is what we will vary based on the condition variable
      resultText <- String()

      switch conditionVariable
        case < 12
          resultText: "Moderate"
        case > 10*multiplier
          resultText: "Very High"
        case 25, 26, 27
          resultText: "Slightly High"
        case currentTemperature("GB"), 21, 22, 23, 24
          resultText: "Perfect"
        default
          resultText: "Not Suitable"

      stdout.println(resultText)

      //The same switch could have been written using given and when
      given conditionVariable
        when < 12
          resultText: "Moderate"
        when > 10*multiplier
          resultText: "Very High"
        when 25, 26, 27
          resultText: "Slightly High"
        when currentTemperature("GB"), 21, 22, 23, 24
          resultText: "Perfect"
        default
          resultText: "Not Suitable"

      stdout.println(resultText)

    returningSwitch()

      stdout <- Stdout()
      //This is what we will use to 'switch on'
      conditionVariable <- "Name"
      //This is what we will vary based on the condition variable

      resultText <- switch conditionVariable
        <- result String: String()
        case 'D'
          result: "Inappropriate"
        case matches /[nN]ame/
          result: "Perfect"
        case > "Gandalf"
          result: "Moderate"
        case < "Charlie"
          result: "Very High"
        default
          result: "Suitable"

      stdout.println(resultText)
//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.

The switch in EK9 has been given great power and versatility - take care with it. Elect to do the simplest matching where ever possible.

In the returningSwitch 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.

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

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 wide spread use of Exceptions; these tend to pervade all API's and force alterations in interfaces. EK9 prefers the use of unSet variables 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. 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

There is quite a bit of flexibility with for loops, but 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 and integer variable. There are a couple of things to note here:

#!ek9
defines module introduction
  defines program
    integerForLoop()
      stdout <- Stdout()
      
      //First example uses interpolated String, second one '+' operators
      
      for i in 1 ... 10
        stdout.println(`Value [${i}]`)
         
      for i in 1 ... 9 by 2
        stdout.println("Value [" + $i + "]")        
//EOF

Please note the spaces around '...' this is required.

For Loop with other types

In the above example an Integer was used, these examples below show the for loop has much more flexibility however.

#!ek9
defines module introduction
  defines program
    floatForLoop()
      stdout <- Stdout()
      
      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 every half hour
      start <- Time().startOfDay() + PT9H
      end <- Time().endOfDay() - PT6H30M
      thirtyMinutes <- PT30M
      
      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.

For Loop with collections

Unlike some other Oriented languages; EK9 does not attach for loop syntax to Collection objects. 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 type
    List of String
    List of Character

  defines program
    collectionsForLoop()
      stdout <- Stdout()
            
      for item in ["Alpha", "Beta", "Charlie"]
        stdout.println(item)

      //The alternative is when you already have an iterator
      moreItems <- ['A', 'B', 'C']
      for item in moreItems.iterator()
        stdout.println(item)
//EOF

Again 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 that has two methods below (which is a little bit 'Duck Type' -ish):

The example above shows a List of String being used in a for loop and also the Iterator from a List of Characters.

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
  defines type
    List of String
    List of Character

  defines function
    workHours()
      -> time as Time
      <- workTime as Boolean: time < 12:00 or time > 14:00
      
    timePipeLine()
      stdout <- Stdout()
      
      //From 9-5 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

So 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 unbundle 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 you would have used an if statement to determine the work hours, this has been implemented with a simple boolean expression 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 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.

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 type
    List of String
    List of Character
    
  defines program  
    collectionsPipeLine()
      stdout <- Stdout()
 
      cat ["Alpha", "Beta", "Charlie"] > stdout

      //The alternative is when you already have an iterator
      moreItems <- ['A', 'B', 'C']
      iter <- moreItems.iterator()
      cat iter > stdout       
//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. There is much more detail on pipeline processing in section Streams/Pipelines.

Simple loops

The final two loop flow control mechanisms are very simple and traditional in approach. They both work on the value of a conditional boolean check.

Traditional While Loop

A short example of a while loop, iterating over a collection of Characters.

#!ek9
defines module introduction
  defines type
    List of Character
    
  defines program
    collectionsWhileLoop()
      stdout <- Stdout()
      
      moreItems <- ['A', 'B', 'C']
      itemIter1 <- moreItems.iterator()
      
      while itemIter1.hasNext()
        item <- itemIter1.next()
        stdout.println(item)      
//EOF

Traditional Do While Loop

This processing is similar to the above except the conditional logic is at the end of the processing block.

#!ek9
defines module introduction
  defines type
    List of Character
    
  defines program
    collectionsDoWhileLoop()
      stdout <- Stdout()
      
      moreItems <- ['A', 'B', 'C']
      itemIter1 <- moreItems.iterator()
      
      //Alternative syntax rather than calling hasNext()
      if itemIter1?
        do          
          stdout.println(itemIter1.next())  
        while itemIter1?             
//EOF

Summary

EK9 has a wide range of flow control features, but it has deliberately excluded some of the more traditional syntax. This is by design, as it is viewed that some of these elements lead to complexity and defects (a bit like GOTO). There are alternative ways to solve the same problems; EK9 encourages a more functional approach.

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.

Next Steps

The next section on Exceptions and try blocks show how error flow control can be implemented.