Getting Started with EK9 · Chapter 3

Making Decisions and Repeating Work

So far the programs have run straight down, top to bottom, doing the same thing every time. Real programs need to choose: they behave differently depending on the values they are given. They also need to repeat: do something many times without writing it out many times. This chapter covers both. It also shows one of EK9's less expected choices: some familiar features are left out, because clearer and safer tools take their place.

Making a choice with if

The if statement runs a block of instructions only when a condition is true.

#!ek9
defines module decisions

  defines program

    Weather()
      -> temperature as Integer

      stdout <- Stdout()

      jacketBelow <- 18
      if temperature < jacketBelow
        stdout.println("Bring a jacket")

Two lines need explaining.

The line -> temperature as Integer means the program is given a temperature when it runs, so it can react to different values. The next section shows how that works and how to run a program with a value. For now, read it as: a temperature comes in.

The line jacketBelow <- 18 may look odd. Why not write if temperature < 18? Because EK9 will not let you compare against a bare number like 18; you must give it a name first. The name does real work: it says what 18 means. "Below the jacket temperature" is clearer than a lone 18 inside a condition. Two values are exempt, because they seldom hide a meaning: 0 and 1. So countdown > 0, later in this chapter, needs no name.

Now the condition itself. temperature < jacketBelow uses the < operator to compare two numbers. It gives back a Boolean, true or false. When the condition is true, the indented block runs; when it is false, the block is skipped.

Note. EK9 also refuses a different mistake here. Suppose you had written temperature <- 14 and then if temperature < 18. EK9 would not build it: the compiler can see that 14 is always less than 18, so the answer never changes. An if whose answer is already known is pointless, and usually a sign of a real bug. That is why the temperature comes in from outside: so the answer depends on the value.

You can offer another path with else, and test more conditions with else if. Each number compared against gets a name, for the same reason as before:

warm <- 25
cold <- 10
if temperature > warm
  stdout.println("It is warm")
else if temperature < cold
  stdout.println("It is cold")
else
  stdout.println("It is mild")

EK9 checks each condition from the top. The first one that is true wins: its block runs, and the rest are skipped. If none is true, the else block runs. Exactly one of the three messages prints, every time.

Note. EK9 writes this as else if, two ordinary words. Some languages use one word, such as elif; in EK9 that is a syntax error.

Giving a program a value

The weather program needs a temperature. You give it to the program when you run it.

Look again at the first lines of the program:

    Weather()
      -> temperature as Integer

The -> means "this comes in". So temperature as Integer says: the program is given one value, a whole number, called temperature. To run it with a temperature of 12, type this at the command line:

ek9 Weather.ek9 12

On the command line, everything you type is text: 12 is the characters "1" and "2". The program asked for an Integer, so EK9 turns that text into an Integer before your code runs. If you type something that is not a whole number, EK9 stops with a message instead of running with nonsense. The value is checked and correctly typed from the moment it arrives.

A program can ask for other types too, and EK9 converts the text you type: String, Integer, Float, Boolean, Character, Date, Time, DateTime, Duration, Money, Dimension, Colour, and a few more. A program could ask for -> chosenDay as Date, and you would pass a date like 2027-03-14. (Typed as 2027-3-14, it is refused: the month and day each need two digits.)

A program can also take several values, read files, and handle options like -v; the chapter Programs, Input and Output covers those. One value coming in is enough for now.

The same idea, spelled when

EK9 lets you write when in place of if. The two behave the same:

when temperature < jacketBelow
  stdout.println("Bring a jacket")

Code reads better when the word fits the meaning. if suits an either/or choice; when reads more naturally for "whenever this is true, do this". Use whichever makes the line clearer.

Joining conditions

You can join conditions with and, or and not, which mean what they mean in English:

chilly <- 18
hot <- 26
if temperature > chilly and temperature < hot
  stdout.println("Pleasant")

This block runs only when both parts are true. Use or when either part is enough, and not to reverse a condition. When a condition grows long, you may break it across several lines, lining the parts up under the first one.

Choosing among many values with switch

A long chain of if / else if is hard to read when you are checking one value against many possibilities. For that, use switch:

#!ek9
defines module decisions

  defines program

    //Choosing among several values with 'switch'.
    //One 'case' can list several values, and there is no fall-through.
    DayKind()
      -> day as Integer

      stdout <- Stdout()

      switch day
        case 1, 2, 3, 4, 5
          stdout.println("Weekday")
        case 6, 7
          stdout.println("Weekend")
        default
          stdout.println("Not a day of the week")

switch day looks at the value of day. Each case lists one or more values to match. The first matching case runs, and the default case runs when nothing else matched. Run it with 6 and it prints:

Weekend

Three things here are deliberate.

First, one case can list several values: case 1, 2, 3, 4, 5 matches any weekday, so you do not need five separate cases.

Second, there is no "fall-through". In some older languages, a switch keeps running into the next case unless you add a word (break) to stop it, and forgetting that word is a classic bug, recorded both in the CERT C coding standard (recommendation MSC17-C) and in the weakness list CWE-484, "Omitted Break Statement in Switch". In EK9 the matching case runs, and that is all.

Third, the default is required. Every switch must end with one, so there is always an answer for a value no case listed. Leave it out and the program does not build (E07320: 'default' is required in this 'switch' statement). No unhandled value slips through unnoticed.

Cases can compare, not just match

So far each case has listed exact values. case 6, 7 means "is the value 6, or is it 7?" — a test of equality, and often all you need.

A case can also compare, using <, > or any of EK9's comparisons:

#!ek9
defines module decisions

  defines program

    Grade()
      -> score as Integer

      stdout <- Stdout()

      passMark <- 40
      meritMark <- 60
      distinctionMark <- 75

      switch score
        case < passMark
          stdout.println("Fail")
        case < meritMark
          stdout.println("Pass")
        case < distinctionMark
          stdout.println("Merit")
        default
          stdout.println("Distinction")

Run it with a score of 55:

Pass

Here order matters. A score of 55 is below 60 and below 75, so two cases are true at once. EK9 checks the cases from the top down, and the first one that matches wins; nothing below it is looked at. 55 fails the first test (< 40), passes the second (< 60), and stops there, printing Pass.

So write comparison cases from the lowest boundary upward, letting each case assume the ones above it have already ruled out the lower values. Put < 75 first and every passing score would match it; nobody would ever see Merit or Distinction. The order is part of the meaning.

So switch is two tools under one word. With value cases (case 6, 7) it is a lookup where order does not matter, because a value can equal only one thing. With comparison cases (case < 60) it is a chained if / else if, checked top to bottom, where order is everything. You can mix the two in one switch. In both, default is the final else: the catch-all that runs when no case above it matched.

Note. Because equality is the most common test, a bare case 6 is shorthand for case == 6. You can write the == out when it makes the intent clearer.

Coming later. Here the switch is on plain numbers. Often you are really choosing among a small, fixed set of named things — the suits in a deck of cards, the days of the week, the states of a traffic light. EK9 lets you define such a set, called an enumeration, and switch works well with one. The chapter Records and Enumerations brings this switch back with real names like Monday in place of 1.

Repeating with a range

To do something a fixed number of times, count over a range with for:

#!ek9
defines module repetition

  defines program

    Counting()
      stdout <- Stdout()

      for i in 1 ... 10
        stdout.println(`Value [${i}]`)

1 ... 10 is a range from 1 to 10, including both ends. The loop runs its block once for each value, and each time the name i holds the current number. This prints Value [1] through Value [10], ten lines. The loop makes i itself; you do not declare it.

You can change the step with by:

for i in 0 ... 10 by 2
  stdout.println(`Even [${i}]`)

This counts 0, 2, 4, 6, 8, 10. A negative step counts down:

for i in 10 ... 0 by -2
  stdout.println(`Down [${i}]`)

Repeating while something is true

Sometimes you do not know how many times to repeat; you only know the condition for carrying on. Then use while:

countdown <- 5
while countdown > 0
  stdout.println(`${countdown}...`)
  countdown := countdown - 1
stdout.println("Lift off")

The while block runs as long as its condition is true. Each pass prints the number, then makes countdown one smaller with :=. After five passes countdown > 0 is false, and the loop stops.

Running at least once: do / while

A plain while checks its condition first, so if the condition is false at the start, the block never runs. When the block should run once before the check, use do / while:

#!ek9
defines module repetition.countup

  defines program

    //'do ... while' runs the block once, then keeps going while the test is true.
    Countup()
      -> stopAt as Integer

      stdout <- Stdout()

      n <- 1
      do
        stdout.println(`${n}`)
        n++            //short for n := n + 1
      while n <= stopAt

The do block always runs at least once. Then the while line is checked, and the block repeats as long as it is true. Run it with a stopAt of 3 and it prints:

1
2
3

stopAt is a value the program is given when it runs, like the temperature earlier. n++ is short for n := n + 1, "add one to n"; n-- takes one away. The only difference from a plain while is when the check happens: while checks before the first run, do / while after it.

The features that are not here

Many languages have three instructions for jumping out of the middle of code: break, to leave a loop early; continue, to skip to the next pass; and return, to leave a block of code partway through. EK9 has none of them. This is deliberate.

The reason is evidence, not taste. Jumps out of the middle make it hard to see, from the shape of the code, which lines will run — the argument Edsger Dijkstra made against unstructured jumps in 1968.1 The results are loops that exit at the wrong moment, checks that get skipped, and security holes from leaving a block earlier than intended. One well-known case, "goto fail", skipped a security check on Apple's iOS and macOS because of a single stray jump.2 Rather than warn you about misusing these tools, EK9 removes them: the idea from Hello, EK9, applied to control flow.

What you use instead are tools that say what you mean:

The first two replacements come from EK9's stream pipelines, which process a collection of data in a line that reads almost like a sentence:

// cat = "take each item", filter = "keep the ones that match"
//   cat readings | filter by inComfortRange | collect as List of Integer

Pipelines have a chapter of their own. Nothing was taken away without something clearer put in its place.

A map of what is coming

You now have the everyday tools for choosing and repeating. EK9 has a few more ways to control what happens and in what order. They work differently, and each gets its own chapter, but it helps to know they exist:

Try it yourself

  1. Write a program that prints whether a number is negative, zero, or positive. Use if / else if / else. (The number must come in to the program, and any value you compare against needs a name, except 0 and 1.)
  2. Use a switch to turn a month number (1–12) into its season. Group the months with multi-value cases, and use default for anything out of range.
  3. Use a for loop to print the two-times table, from 2 to 20.
  4. Rewrite exercise 3 as a while loop, then as a do / while loop. Which reads most clearly to you, and why?

Recap

You can make decisions with if, else if, else and when, join conditions with and, or and not, and choose among many values with switch: several values per case, comparison cases like case < 40, first match wins, and no fall-through. You can repeat work with for over a range, with while, and with do / while when the block must run at least once. EK9 has no break, continue or return, and you have seen what replaces them. And you have a map of the larger control-flow ideas to come: stream pipelines, exceptions, guards and expression forms.

Remove the sharp edge, supply a better tool: that pattern recurs throughout EK9. Functions are next, so you can name and reuse pieces of logic instead of repeating them.

  1. Edsger W. Dijkstra, "Go To Statement Considered Harmful", Communications of the ACM, 1968 — the classic argument for structured control flow over unstructured jumps. See the evidence appendix for plain-language notes. ↩

  2. The Apple "goto fail" bug (2014, CVE-2014-1266): a duplicated goto fail; line skipped TLS signature verification on iOS and macOS, weakening secure connections. It is one of the clearest real-world arguments for removing mid-block jumps from a language. ↩