Flow Control in EK9

EK9 has the flow control you would expect: if, switch, for, while and do/while. It also has streams and guards, so you need the imperative (traditional) statements less often, and it adds features to the statements themselves.

This article explains why a function or method body has no early returns.

What's Different: EK9's Control Flow Philosophy

Coming from Java, Python, C++ or C#? EK9's control flow will feel restrictive at first. You will reach for break, continue or return and find the compiler rejects them.

The Missing Statements

EK9 does not have:

They are not an oversight. The grammar (EK9.g4) recognises these words only to reject them with their own error; no statement uses them.

Why They're Gone: The Evidence

Each of them is a jump out of the structured flow of a program, and jumps that compile cleanly and do the wrong thing have a long public record: Dijkstra's "Go To Statement Considered Harmful", the CERT and CWE entries on a forgotten break, the Linux kernel's fall-through audit and Apple's goto fail. Design note: why EK9 has no break, continue or return sets out the record and the argument.

EK9's Alternatives

Each statement EK9 leaves out has a replacement that states its intent and that the compiler can check.

1. Stream Pipelines Replace break/continue

Traditional (break/continue) EK9 (Stream Pipeline)

Fails with E01070: 'break' does not exist in EK9 - use stream pipelines with 'head' or guards

found <- false
for item in items
  if startsWithA(item)
    found: true
    break
stdout.println(found)
firstMatch <- cat items
  | filter by startsWithA
  | head 1
  | collect as List of String
found <- not firstMatch.empty()
stdout.println(found)

Why better: the intent is explicit (head 1 means "take the first match"), no loop state changes, and you cannot break out of the wrong loop.

2. Guards Replace Early Returns

Early return (rejected) EK9 (Guard)

Fails with E01072: 'return' does not exist in EK9 - declare return values with '<-' and compiler ensures all paths initialize

#!ek9
defines module bad.site.functions.noreturn.keyword

  defines function

    sign() as pure
      -> amount as Integer
      <- rtn as String?

      if amount < 0
        return "negative"
      return "not negative"
//A guard replaces the early return: the body runs only when lookup gives a set value,
//otherwise the initial value stands.
countryName() as pure
  -> code as String
  <- rtn as String: "unknown"

  if name <- lookup(code)
    rtn: name

Why better: the compiler checks that the returning value is given a value on every path, so you cannot leave without setting it.

3. Multiple Case Values Replace Switch Fallthrough

Traditional (fallthrough) EK9 (Multiple Values)
switch day
  case MONDAY:
  case TUESDAY:
  case WEDNESDAY:
    type = "weekday"
    break  // ❌ Forget this = fallthrough bug
  case SATURDAY:
  case SUNDAY:
    type = "weekend"
    break
switch day
  case Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY
    dayType: "weekday"
  case Day.SATURDAY, Day.SUNDAY
    dayType: "weekend"
  default
    dayType: "unknown"
  // Automatic break - can't fall through

Why better: the values that share a case are listed together, nothing can fall through by accident, and there is less to write.

4. Return Value Declarations Replace return Statements

You declare the returning value in the signature and assign to it in the body. The compiler checks that every path gives it a value (Returning a value: there is no return).

//No initial value: the compiler proves that every path gives it one.
sign() as pure
  -> amount as Integer
  <- rtn as String?

  if amount < 0
    rtn: "negative"
  else if amount > 0
    rtn: "positive"
  else
    rtn: "zero"

A path that leaves the returning variable without a value is an error:

Fails with E08050: return value is not always initialised

#!ek9
defines module bad.site.functions.noreturn.missedpath

  defines function

    sign() as pure
      -> amount as Integer
      <- rtn as String?

      if amount < 0
        rtn: "negative"
      else if amount > 0
        rtn: "positive"

The value can come from a switch, which returns its value through its own <- variable (multiple case values, no fallthrough):

//A switch returns a value through its own '<-' variable.
dayKind() as pure
  -> day as String
  <- rtn as String: switch day
    <- kind as String?
    case "Saturday", "Sunday"
      kind: "weekend"
    default
      kind: "weekday"

A stream pipeline replaces the loop that would need continue or break:

//A stream replaces a loop with 'continue' or 'break': 'filter' skips items ('head n' would stop
//early), and '>' adds each Integer into the returning variable.
sumOfPositives() as pure
  -> amounts as List of Integer
  <- rtn as Integer: 0

  cat amounts | filter by isPositive > rtn

Why better: you cannot forget to return, or return the wrong value by leaving early; the compiler proves every path sets the returning value.

What This Means For You

Getting Started

You do not lose capability: every pattern you used break, continue or return for has a more explicit alternative that the compiler can check.

Tip: when you want to "break early", ask what you are actually looking for. Then say so with stream operations: filter or reject to choose, skip to pass over, and head to stop.

Why EK9 does it this way. Removing a statement removes a whole category of bug by construction, not by measurement: there is no break to put in the wrong loop, no case that can fall through (the CERT MSC17-C / CWE-484 defect cannot be written), no path that forgets its returning value, and no goto fail. The aim is code with fewer bugs whose reviews go faster, because the control flow is explicit and cannot be misread. Swift, Go, Rust and Kotlin already have no implicit switch fallthrough; EK9 goes further - if it's dangerous, it doesn't exist.

See Also

The Controls

With streams and the EK9 operators you need traditional flow control much less, though some cases still require it. Each control below starts with its simplest form and then shows what EK9 adds.

Why EK9 does it this way. EK9 favours a more functional approach to flow control, in the view that it lowers the developer's cognitive load, is simpler and more flexible, and gives less code, more reuse and fewer defects.

EK9 Guard System Quick Reference

One guard system removes the need for hand-written null checks and shortens null-safety code. Each guard does several checks for you, in place of the five to seven lines in the examples below, and the compiler generates the checks with short-circuiting.

Operator Name Checks Semantics Example
<- Declaration guard ✅ Null check
✅ isSet check
Declare a NEW variable, assign, enter only if it is SET if name <- getName()
:=? Assign-if-unset ✅ isSet check (target, before)
✅ Lazy evaluation
✅ isSet check (target, after)
Fill the variable only if it is unset - the expression is not even evaluated otherwise. The body runs if the variable is SET afterwards (a variable that already had a value keeps it and enters the body) if cache :=? compute()
:= then x? Assignment, then a set-check ✅ isSet check (written out) := is a blind assignment: it ALWAYS assigns (an unset result replaces the old value) and checks nothing, so an if needs its own control (E07340 without one). then x? re-points the variable and enters only if the new value is SET if fetched := fetch() then fetched?

The names - one per symbol, used everywhere in EK9:

A guard means "enter only if set", like Swift's if let. There is no ?= operator: an assignment never yields a value.

Key Insight: the same guard syntax works in if, switch, for, while, do-while and try, in both statement and expression form. Learn the pattern once and apply it everywhere. In expression form the guard gates the construct: if it skips the body, the result is the returning variable's initial value (the initialiser is the guard's "else"). So the returning variable must be initialised (<- rtn as T: default), or the compiler stops you with E08050.

A guard over a Boolean tests its truth. In an if or else if, a <- or :=? head whose variable is declared Boolean enters only when the variable is set and true. if allowed <- permitted(user) skips the body when permitted returns false, as it does when the result is unset, so no then allowed is needed.

Outside a head an assignment never yields a value, so assign and then check:

:=? on a variable that is always set is rejected (E08095): it can never assign. :=? always leaves an object in its target - present, maybe unset, never absent - so where it always runs it initialises a variable declared with no value.

Quick Reference: Choosing the Right Guard Operator

Follow this decision tree:

┌─ First time using this variable?
│  └─ YES → Use `<-` (declaration guard)
│     • Declares NEW variable
│     • Checks if value is SET
│     • Example: if name <- getName()
│
│  └─ NO → Variable already exists...
│     ┌─ Is variable already set with value you want to keep?
│     │  (e.g., lazy cache, don't re-compute if already has value)
│     │  └─ YES → Use `:=?` (assign-if-unset)
│     │     • Checks LEFT side first (is target unset?)
│     │     • LAZY EVALUATION - only calls expression if left unset
│     │     • Perfect for caching/lazy init patterns
│     │     • Example: if cache :=? compute() with cache > 100
│     │
│     └─ NO → Need to update/replace value...
│        ┌─ Need to verify new value is valid before using?
│        │  (e.g., fetching data that might fail)
│        │  └─ YES → Use `:=` then `x?` (assignment, then a set-check)
│        │     • Evaluates expression and assigns it ALWAYS
│        │       (an unset result replaces the old value)
│        │     • Body runs only if the result is SET
│        │     • Example: if fetched := fetch() then fetched? and (fetched.isValid())
│        │
│        └─ NO → Use `:=` (blind assignment)
│           • No set-check of its own
│           • Use when an explicit condition decides
│           • Example: if count := getCount() then count > 0
Scenario Operator When to Use
First use of variable <- Declaration + automatic guard check
Lazy cache pattern :=? Don't re-evaluate if already set + guard check
Update with validation := then x? Always evaluate and assign, then the explicit set-check
Known safe value := No guard (value certain or explicit condition validates)

Key Principle: use a guard (<-, :=?) whenever you need to check that a value is SET. Use := (blind assignment) with an explicit condition - then x? when the condition is that the new value is set.

Why EK9 does it this way. Other languages each give part of this:

  • Java: Optional chains with .map().filter().orElse()
  • Kotlin: null-safe operators, but no guard that works the same in every control
  • Rust: pattern matching with if let Some(x) = expr { }
  • Swift: if let and guard statements

EK9 combines them into one system that works the same in every control flow construct. The result is far less guard code than the Java equivalent (see the line counts below), and the checks are written by the compiler, so none can be forgotten.

What EK9 Guards Replace

Each example compares null-safe code in a traditional language with the one line of EK9 that replaces it.

Example 1: Declaration Guard (<-)
Traditional Java/C++ (3 levels of nesting) EK9 (1 line)
value = getOptional();
if (value != null && value.isSet()) {
  if (value > 10) {
    // body
  }
}
if value <- getOptional() with value > THRESHOLD
  stdout.println(`declared ${value}`)

Savings: 5 lines → 1 line, 3 levels → 1 level, and the checks are made by the compiler.

Example 2: Re-point and check (:= then x?)
Traditional (3 levels of nesting) EK9 (1 line)
existing = fetchData();
if (existing != null && existing.isSet()) {
  if (existing.isValid()) {
    // body
  }
}
if existing := fetchData() then existing? and (existing.isValid())
  stdout.println("fetched a valid record")

Savings: 5 lines → 1 line, 3 levels → 1 level.

Example 3: Assign-if-unset (:=?)
Traditional (2 levels of nesting) EK9 (1 line)
if (cache == null || !cache.isSet()) {
  cache = compute();
}
if (cache != null && cache.isSet()) {
  if (cache > 100) {
    // body
  }
}
if cache :=? compute() with cache > CACHE_LIMIT
  stdout.println(`cached ${cache}`)

Savings: 7 lines → 1 line, 2 levels → 1 level.
Bonus: lazy evaluation - compute() is only called if cache is unset.

Guard Operator Mechanics - What Happens Under the Hood

These are the steps the compiler generates for each guard; you write none of them.

Declaration Guard <- with Condition
if value <- getOptional() with value > THRESHOLD
  stdout.println(`declared ${value}`)

Compiler-Generated Steps:

  1. Call getOptional()
  2. Assign result to value
  3. NULL CHECK: Is value null? → If yes, skip to else
  4. isSet CHECK: Call value._isSet() → If false, skip to else
  5. Short-circuit AND: Evaluate value > THRESHOLD ONLY if isSet was true
  6. Execute body ONLY if all checks passed
Assignment := then a set-check, with Condition
if existing := fetchData() then existing? and (existing.isValid())
  stdout.println("fetched a valid record")

Compiler-Generated Steps:

  1. Call fetchData()
  2. ALWAYS assign: existing = result - an unset result replaces whatever existing held
  3. isSet CHECK (written out as existing?): Call existing._isSet() → If false, skip to else
  4. Short-circuit AND: Evaluate existing.isValid() ONLY if existing is set
  5. Execute body ONLY if all checks passed
Assign-if-unset :=? with Condition (Most Complex)
if cache :=? compute() with cache > CACHE_LIMIT
  stdout.println(`cached ${cache}`)

Compiler-Generated Steps:

  1. LEFT SIDE CHECK: Is cache unset? → If it is already SET, keep its value and go straight to step 4
  2. LAZY EVALUATION: ONLY NOW call compute()
  3. ASSIGN: cache = result
  4. isSet CHECK: Call cache._isSet() → If false, skip to else
  5. Short-circuit AND: Evaluate cache > CACHE_LIMIT ONLY if cache is set
  6. Execute body ONLY if all checks passed - a cache that already had a value enters the body with it

Note: compute() is never called if cache already has a value. That lazy evaluation matters for expensive operations.

Key Benefits of EK9 Guards:

See also: tri-state semantics, which guards are built on, and the examples throughout this page.

The traditional flow control falls into three groups:

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

Most of the processing above can also be written in this more functional way.

Control examples

Require - Hard Contracts

Before the branching and looping constructs, understand require. It enforces the conditions a program needs in order to be correct.

Unlike Java, where assertions can be disabled, EK9 requirements are hard requirements. They are part of the language's safety model and are never removed from production code: every require generates an instruction that always executes.

Use require for "Design by Contract": pre-conditions, post-conditions and invariants. A failed requirement halts execution, so the program cannot continue in an invalid state; it cannot be caught.

#!ek9
defines module site.flowControl.require

  defines function

    calculateSqrt()
      -> radicand as Float
      <- rtn as Float?

      //This is a pre-condition contract. The function cannot accept a negative value.
      require radicand >= 0.0

      //... function logic
      rtn: sqrt radicand

If statements

Before you write an if, look at ternary operators and assign-if-unset; they are often terser. The contrast of if/else and ternary operators covers the common cases, especially variables that may be un-set. When you do need an if, EK9 adds features that give you more control.

Simple If statements

The 'if/else' in its simplest form.

#!ek9
defines module site.flowControl.ifelse

  defines constant
    UPPER_LIMIT <- 21
    LOWER_LIMIT <- 10

  defines function

    testIfElseInteger()
      -> first as Integer

      trigger <- false

      if first > UPPER_LIMIT
        trigger: true
      else if first < LOWER_LIMIT
        trigger: false
      else
        trigger: Boolean()

      //Just ensure referenced
      require trigger?

    testIfElseIntegerAsPure() as pure
      -> first as Integer

      trigger <- Boolean()

      if first > UPPER_LIMIT
        trigger :=? true
      else if first < LOWER_LIMIT
        trigger :=? false

      //Just ensure referenced
      require trigger?

In testIfElseInteger the value of trigger depends on the value of first.

testIfElseIntegerAsPure shows the pure keyword, which is stricter about reusing variables and assigning to them. :=? assigns only when the left-hand side is un-set.

Range

A kind of if statement with one specific use: checking whether a value is, or is not, within a range. It is terse for simple cases.

#!ek9
defines module site.flowControl.rangecheck

  defines constant
    LIMIT <- 16
    OFFSET <- 2

  defines function

    kotlinLikeRangeCheck()
      ->
        candidate as Integer
        lowerBound as Integer
        upperBound as Integer

      stdout <- Stdout()

      isWithin <- candidate in lowerBound ... upperBound
      stdout.println(`${candidate} is within a range of ${lowerBound} ... ${upperBound}: ${isWithin}`)

      isNotWithin <- candidate not in LIMIT - OFFSET ... upperBound - 1

      stdout.println(`${candidate} is NOT within a range of ${LIMIT - OFFSET} ... ${upperBound - 1}: ${isNotWithin}`)

The start and end of the range can be calculated values or expressions. The check is similar to a ternary or a very simple if expression; where the conditions are more complex, EK9 uses switch.

If statement with boolean logic

Combined boolean logic in an if statement, with alternative syntax and layout.

#!ek9
defines module site.flowControl.simpleif

  defines constant
    LOW_LIMIT <- 10
    HIGH_LIMIT <- 19
    TARGET <- 9

  defines function

    currentTemperature() as pure
      -> country as String
      <- temperature as Integer: Integer()

      if country == "GB"
        temperature := 20
      else if country == "DE"
        temperature := 41

    simpleIf()
      -> valueToTest as Integer
      stdout <- Stdout()

      suitableValue <- String()

      if valueToTest < LOW_LIMIT
        stdout.println(`Value ${valueToTest} is less than ${LOW_LIMIT}`)

      secondValue <- 21
      specialCondition <- true

      if valueToTest < LOW_LIMIT and secondValue > HIGH_LIMIT or specialCondition
        stdout.println("Test OK")

      //The same logic as above - but in a different layout.
      if valueToTest < LOW_LIMIT and
      secondValue > HIGH_LIMIT or
      specialCondition
        stdout.println("Test OK")

      //Rather than use the keyword 'if' you can use 'when'
      when valueToTest < LOW_LIMIT
        stdout.println(`Value ${valueToTest} is less than ${LOW_LIMIT}`)

      //As you would expect it is possible to chain if and else statements
      if valueToTest > TARGET
        suitableValue: "Too High"
      else if valueToTest < TARGET
        suitableValue: "Too Low"
      else
        suitableValue: "Just Right"

      stdout.println(suitableValue)

If statement with assignment/guard and declaration

Like the switch (see later), the if statement takes a guard, an assignment or a declaration as part of its structure.

#!ek9
defines module site.flowControl.assignmentinif

  defines constant
    HOT_LIMIT <- 50
    WARM_LIMIT <- 40
    UNKNOWN_WARMTH <- "Not sure where it might be warm"

  defines function

    currentTemperature() as pure
      -> country as String
      <- temperature as Integer: Integer()

      if country == "GB"
        temperature := 20
      else if country == "DE"
        temperature := 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 > HOT_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the US`)
      else when selectedTemperature := currentTemperature("US") with selectedTemperature < HOT_LIMIT
        stdout.println(`Temp of ${selectedTemperature} not too bad in the US`)
      else when selectedTemperature := currentTemperature("GB") with selectedTemperature > WARM_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the UK`)
      else when selectedTemperature := currentTemperature("DE") with selectedTemperature > WARM_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the DE`)
      else
        stdout.println(UNKNOWN_WARMTH)

    assignAndCheckInIf()
      stdout <- Stdout()

      selectedTemperature <- Integer()

      //Here ':=' assigns, 'selectedTemperature?' checks for unset and only then does the further condition run
      //Also note we can still use 'if' and rather than 'with' use 'then'
      when selectedTemperature := currentTemperature("US") then selectedTemperature? and (selectedTemperature > HOT_LIMIT)
        stdout.println(`Temp of ${selectedTemperature} a little warm in the US`)
      else when selectedTemperature := currentTemperature("US") then selectedTemperature? and (selectedTemperature < HOT_LIMIT)
        stdout.println(`Temp of ${selectedTemperature} not too bad in the US`)
      else if selectedTemperature := currentTemperature("GB") then selectedTemperature? and (selectedTemperature > WARM_LIMIT)
        stdout.println(`Temp of ${selectedTemperature} a little warm in the UK`)
      else when selectedTemperature := currentTemperature("DE") then selectedTemperature? and (selectedTemperature > WARM_LIMIT)
        stdout.println(`Temp of ${selectedTemperature} a little warm in the DE`)
      else
        stdout.println(UNKNOWN_WARMTH)

    declarationInIf()
      stdout <- Stdout()
      when selectedTemperature <- currentTemperature("US") with selectedTemperature > HOT_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the US`)
      else when selectedTemperature := currentTemperature("US") then selectedTemperature? and (selectedTemperature < HOT_LIMIT)
        stdout.println(`Temp of ${selectedTemperature} not too bad in the US`)
      else if selectedTemperature := currentTemperature("GB") with selectedTemperature > WARM_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the UK`)
      else when selectedTemperature: currentTemperature("DE") then selectedTemperature > WARM_LIMIT
        stdout.println(`Temp of ${selectedTemperature} a little warm in the DE`)
      else
        stdout.println(UNKNOWN_WARMTH)

If statement summary

The examples show:

Note: a variable declared in an if or else if is in scope in the later else if and else blocks (it belongs to the main if scope block). Be sure you want this: it can be confusing and hard to refactor later.

The ':=?' guard, the 'x?' set-check and '<-' need the variable's type to have the '?' is-set operator, so that the guard can evaluate whether the result is set.

An assignment within the if or else if means fewer variables to create and assign before the if block. With a guard, the condition part is not evaluated if the guard's variable is isSet == false. A guard over a Boolean (if allowed <- permitted()) enters only when the value is set and true.

A declaration, assignment or assign-if-unset often reads better with when than with if, but that is your choice. Ternary operators, assignments and guards add a lot to conditional flow control, and the switch statement adds more. Guards are most useful with methods and functions that may return un-set values, because they keep the extra checks simple.

When there are many conditions to consider, use the switch statement/expression.

Switch

The EK9 switch is in some ways like if/else: the order of the cases matters. It supports multiple and varied matches, can return a value like an expression, and, like if, has alternative keywords.

Switch on Enumerated Values

This is closest to a switch statement in other languages, but with strict semantics. When the case statements refer directly to enumerated values, all the enumerated values must have a case. A default is still needed for when the value being switched over is un-set.

The example is in 'expression' form; the switch can also be a 'statement'.

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()
    -> selection as LimitedEnum

    result <- switch selection
      <- 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: "Selection is not set"

    require result?

The example defines a simple 'Enumerated type', LimitedEnum, and a switch expression compares the variable 'selection' with each of its values, returning a value from each case.

Used this way the switch is exhaustive: the compiler reports an error if an enumerated value or the default is missing.

Why EK9 does it this way. This may seem too 'strict', but it catches a common error. When a value is added to the 'Enumerated type', existing code stops compiling, and that forces the developer to assess the processing for the new value.

If you do not want to handle each enumerated value explicitly, make one case an expression:

defines function

  SimpleNonExhaustiveEnumerationExpressionSwitch()
    -> selection as LimitedEnum

    result <- switch selection
      <- rtn as String?
      case == LimitedEnum.A
        rtn: "Just A"
      default
        rtn: "Selection is anything but A"

    require result?

A case using the == (equality) operator turns off the exhaustive check. Values added to the 'Enumerated type' later go to the default block. This is the less strict switch of many other languages.

Switch form similar to an if expression

A switch expression can be used like an 'if expression':

#!ek9
defines module site.flowControl.giventernary

  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"

      require resultValue?

      //But above could just have been a ternary
      resultValue2 <- conditionVariable <- "Steve" else "Not Steve"
      require resultValue2?

This is why EK9 has no 'if expression': the switch used as an expression does everything one would. For an expression this simple a 'ternary' (also shown) would do. When the 'when true' or default blocks span several lines a ternary cannot be used, and the switch expression is the right choice.

Switch in statement form

A simple switch statement with expressions in its case statements, and the alternative syntax given/when in place of switch/case. It also uses the 'pure' syntax, in which variables are only assigned to when unset (the :=? syntax).

#!ek9
defines module site.flowControl.switchstatement

  defines function

    currentTemperature() as pure
      -> country as String
      <- temperature as Integer: Integer()

      if country == "GB"
        temperature := 20
      else if country == "DE"
        temperature := 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"

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

      require resultText2?

A case can match much more than single values, so the order of the cases matters: EK9 takes only the first match. Functions used in a match must have no side effects.

There is no break or yield, and no case 'fall-through'; a case takes several conditions instead.

The EK9 switch has many forms; take care with it, and choose the simplest matching you can.

Switch in expression form

A function in 'pure' form again, this time with the switch returning a value as an expression. A switch expression suits 'pure' code, where a variable is assigned only once. The example also shows more case conditions.

#!ek9
defines module site.flowControl.switchexpression

  defines function

    ASwitchAsExpression() as pure
      -> conditionVariable as String

      //This is more like a chained if/else with expressions in the <i>case</i>
      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"

      require resultText?

In ASwitchAsExpression the switch declares a new variable, resultText, and initialises it from the return value of the switch.

String values can be matched by lexical comparison and by regular expression. Again, focus on the order of the cases. In other languages a switch usually matches single values; EK9's range matching does more, but needs more focus to get right.

Switch with a guard/assignment/declaration condition

A guard is like an enclosing 'if statement'. In this example 'temperature' starts 'un-set', so ':=?' (assign-if-unset) fills it from 'currentTemperature'. If that returns 'un-set', 'temperature' stays 'un-set', the switch expression is not evaluated at all, and 'resultText' takes the switch's returning value's initial value (here '<- result String: String()', an un-set String) — the initialiser is the guard's "else". Because a guard can skip the body, the returning value must have an initial value; without one the compiler reports E08050 (return value is not always initialised).

The assignment and declaration forms always process the switch; the declaration is made inside the switch scope.

#!ek9
defines module site.flowControl.switchguards

  defines constant
    MODERATE <- "Moderate"
    VERY_HIGH <- "Very High"
    SLIGHTLY_HIGH <- "Slightly High"
    PERFECT <- "Perfect"
    SUITABLE <- "Suitable"

  defines function

    currentTemperature() as pure
      -> country as String
      <- temperature as Integer: Integer()

      if country == "GB"
        temperature := 20
      else if country == "DE"
        temperature := 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

      require 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

      require 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

      require 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"
      require temperature?

To repeat: an EK9 switch must have a default, and there is no fall through (so no break keyword as in C/C++/C#/Java). Several values per case do the job of fall through. The EK9 switch is much more like an if/else chain than a C, C++ or Java switch.

Try Catch and Exceptions

Try, catch and exceptions are flow control for an error state. Do not use them to control normal flow. An Exception is a form of GOTO (as are break and return, in a slightly more controlled way). See Exceptions/Error Handling for more details.

Why EK9 does it this way. Many languages use exceptions widely, and they spread through every API and force changes to interfaces. EK9 prefers unSet values, Result, and careful, selective use of exceptions. So EK9 code is less likely to throw and more likely to return a value that is unSet or a Result, and the caller must be ready for values that are not set. Throw only in exceptional circumstances, never as a design mechanism for flow control.

For Loops - but with extras

Use a loop to perform the same operations over a set of data. EK9 has no break and no return statement, so you cannot exit a for loop early; that is by design (see the EK9 philosophy).

A for loop runs from end to end. To stop part way through, use a while loop, a streaming for loop or streaming collections.

Standard For Loop

A standard loop over an integer variable:

#!ek9
defines module site.flowControl.forloops

  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
      require result?

The spaces around '...' are required.

A for loop can also be an expression that returns a value, as in the function 'ForLoopAsExpression'.

For Loop with other types

The for loop works with any type, your own included, that provides the comparison, addition and subtraction operators it needs.

#!ek9
defines module site.flowControl.forfloattime

  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}]`)

So the EK9 for loop:

For Loop with collections

Unlike some object-oriented languages, EK9 does not attach for loop or stream syntax to Collection objects. It traverses the objects in a Collection in two complementary ways: the first is below, the second is streaming collections.

#!ek9
defines module site.flowControl.forcollections

  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)

The loop variable's type is inferred from the Collection or Iterator. The for loop works with any type that has an iterator() method returning an Iterator, or (a 'Duck Type' approach) with any object whose type has these two methods:

The example loops over a List of String and over the Iterator of a List of Characters.

For Loop assignments, guards and declarations

As with if and switch, a for loop can take an 'assignment', a 'guard' or a 'declaration', which keeps it consistent with the rest of EK9's flow control:

#!ek9
defines module site.flowControl.forguards

  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
      require prefix?

Streaming 'For'

The values a for loop generates can feed a processing pipeline, here with some filtering.

#!ek9
defines module site.flowControl.timepipeline

  defines constant
    LUNCH_START <- 12:00
    LUNCH_END <- 14:00

  defines function

    workHours() as pure
      -> time as Time
      <- workTime as Boolean: time < LUNCH_START or time > LUNCH_END

  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

Running the program 'TimePipeLine' prints the working half hours:

09:00:00
09:30:00
10:00:00
10:30:00
11:00:00
11:30:00
14:30:00
15:00:00
15:30:00
16:00:00
16:30:00
17:00:00

The for loop generates a sequence of times and pipes them into a pipeline, where the function 'workHours' filters them: only a time for which it returns 'true' goes on to the next stage. So 'stdout' gets only the times before 12:00 and after 14:00.

Without break and return you un-bundle nested loops (always tricky) into pipelines. The logic you might have nested in the for loop moves into a function that you can test, reuse and change easily.

This takes a little time to get used to. Where you would have used an if statement for the working hours, there is a boolean expression (a predicate) in the function 'workHours'. You still write the same expression, plus the function around it, which is more text than an 'if'. But the code has moved from procedural to declarative.

head cuts the stream short: once it has the number of objects you asked for, it shuts the pipeline down, much like break or return. To find a particular value in a collection or a generated list, use a filter to select it and head to stop once one matching value comes through. The value may never come through, if it is not there.

Why EK9 does it this way. The Streaming 'For' is the main reason EK9 has no List Comprehensions. EK9 needs more code than a Python list comprehension, because of the functions, but the code is more readable and fits the EK9 philosophy. It also standardises functional processing: unlike Java, where specific methods are attached to specific 'Functional types', the functional processing syntax is built into the language itself.

Streaming Collections

As with the streaming for, you can send the contents of a collection through a pipeline rather than loop over it.

#!ek9
defines module site.flowControl.collectionspipeline

  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
      require capturedValues?

With the cat command, collection classes need no for/stream syntax of their own, which gives more flexibility and consistency. You 'stream' the contents of a collection, or another source such as an iterator, into a pipeline.

The example does no real processing; it writes the contents to stdout. Part of it streams two 'lists of Strings', uses tee to copy them into a collection for later, and also streams them to stdout. Streams/Pipelines has much more on pipeline processing.

While and Do/While loops

The last two loops are traditional: both depend on a boolean condition. They too take an 'assignment', a 'guard' or a 'declaration', which runs once, when the loop is first reached. If a guard leaves its variable un-set the body never runs; otherwise only the condition is checked again on each iteration. Both can also be expressions that return values.

While loop examples

A while loop over a collection of Characters, using the 'guard', 'assignment' and 'declaration' syntax. The final function, 'whileLoopAsExpression', uses the while loop as an expression with a variable 'declaration'.

#!ek9
defines module site.flowControl.while

  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 re-point the iter from above to a fresh one, and check it is set
      while itemIter := moreItems.iterator() then itemIter? and (itemIter.hasNext())
        item <- itemIter.next()
        stdout.println(item)

      //Same again but with a blind assignment and only the condition
      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()
      limit <- 10
      result <- while complete <- false with not complete
        <- rtn <- 0
        rtn++
        complete: rtn == limit
      require result?

Do/While loop example

As above, except the condition is at the end of the block.

#!ek9
defines module site.flowControl.dowhile

  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: a declaration guard skips even the first pass when the iterator is unset
      do freshIter <- moreItems.iterator()
        item <- freshIter.next()
        stdout.println(item)
      while freshIter.hasNext()

    doLoopAsExpression()
      limit <- 10
      result <- do complete <- false
        <- rtn <- 0
        rtn++
        complete: rtn == limit
      while not complete

      require result?

      //Just to show that 'complete' is bound into the do/while scope
      complete <- "Almost"
      require complete?

The 'guard' makes the do/while block slightly simpler. The do/while expression is a common programming 'pattern', and the 'declaration' in the do/while makes it more self-contained.

Excluded Control Flow Features

EK9 deliberately excludes several control flow features that are common in other languages. The design note gives the record behind the decision.

Features Not in EK9

FeatureWhy ExcludedEK9 AlternativeError Code
break Exits the wrong loop silently; an unstructured jump (Dijkstra, 1968) Stream pipelines with head E01070
continue Skips the rest of a loop body from the middle of it; an unstructured jump Stream pipelines with filter/skip E01071
return An early exit from the middle of a function can skip cleanup and hide where the result is set Return declarations, guards E01072
null Tony Hoare's "billion-dollar mistake" (2009); Java's NullPointerException Tri-state semantics (unset/set) E01073
Semicolons Syntax noise Indentation-based syntax E01074

Instead of break/continue - Use Stream Pipelines

#!ek9
defines module site.flowControl.breakcontinuealternatives

  defines function

    isMatch() as pure
      -> item as String
      <- rtn as Boolean: item matches /a.*/

    isValid() as pure
      -> item as String
      <- rtn as Boolean: length item > 0

    process()
      -> item as String
      Stdout().println(item)

    //BAD: What you might write in other languages
    //for item in items
    //  if isMatch(item)
    //    break  <- ERROR: E01070

    //GOOD: EK9 stream pipeline
    goodBreakAlternative()
      -> items as List of String
      <- result as List of String: cat items | filter by isMatch | head 1 | collect as List of String

    //BAD: continue in other languages
    //for item in items
    //  if not isValid(item)
    //    continue  <- ERROR: E01071
    //  process(item)

    //GOOD: EK9 stream pipeline
    goodContinueAlternative()
      -> items as List of String

      //Filter out invalid items, process the rest
      valid <- cat items | filter by isValid | collect as List of String
      for item in valid
        process(item)

Instead of return - Use Return Declarations

#!ek9
defines module site.flowControl.returnalternative

  defines function

    transform() as pure
      -> payload as String
      <- result as String: payload.upperCase()

    //BAD: Early return in other languages
    //processData()
    //  -> payload as String
    //  <- result as String?
    //  if not payload?
    //    return ""  <- ERROR: E01072

    //GOOD: Return declaration with default
    goodReturnAlternative()
      -> payload as String
      <- result as String: String()  //Default value

      if payload?
        result: transform(payload)
      //Compiler enforces all paths initialize result

Instead of null - Use Tri-State Semantics

#!ek9
defines module site.flowControl.nullalternative

  defines function

    getName() as pure
      <- name as String: "Steve"

    process() as pure
      -> name as String
      <- rtn as String: name.upperCase()

    //BAD: null in other languages
    //badNullUsage()
    //  name <- null  <- ERROR: E01073

    //GOOD: Create unset value
    goodTriStateUsage()
      name <- String()  //Unset
      require not name?

      //Guard checks if set
      if validName <- getName()
        stdout <- Stdout()
        stdout.println(process(validName))  //Guaranteed set

Summary

EK9 has a wide range of flow control, but deliberately leaves out some traditional syntax, such as break, because it leads to complexity and defects (a bit like GOTO). It adds to the traditional flow control syntax:

Choosing a construct:

Next Steps

Exceptions and try blocks show how to handle error flow.