Getting Started with EK9 · Chapter 10

Guards and Safe Code

You already know the central idea of this chapter: a value can be set, unset or absent, and the ? operator asks "is this set?". This chapter builds sharper tools on that one idea. By the end you will write code that checks for a missing value at every step, with the compiler checking that you did, and you will have seen the one pattern EK9 uses for it everywhere.

Each tool is a small step up from the one before.

The pattern that keeps repeating

Here is a small problem. We ask for a name from somewhere that might not have one. If we do not get a usable name, we fall back to a default.

With the tools so far, it looks like this:

name <- lookupName()
if not name?
  name := "Friend"
stdout.println(`Hello, ${name}`)

Declare name from lookupName(). If it is not set, replace it with "Friend". Then greet. This is correct and clear. But you will write this shape — get a value, check whether it is set, act on the answer — many times a day, and EK9 has tighter ways to say it.

Note. lookupName() here is a function that returns a String which may be set or unset. How it decides does not matter; what matters is that the result might not be there.

The one that fits this shape — "use the value, or a fallback if it is missing" — is the ternary, the one-line either/or you met in Speaking Every Language:

name <- lookupName()
displayName <- name? <- name : "Friend"
stdout.println(`Hello, ${displayName}`)

As before, the part before the <- is the question: name?, is name set? After it come the two answers, separated by a colon: if yes, the part before the colon (name); if no, the part after ("Friend"). Either way displayName holds a set value, and the greeting works as before, with the fallback in one line instead of three.

The next tool, the guard, is related but does a different job, and you will use it constantly. After it come more of these set-aware shortcuts.

The guard: check and use in one step

Often you want to do two things together: take a value, and carry on only if it is set. EK9 has a single tool for that, called a guard:

if name <- lookupName()
  stdout.println(`Hello, ${name}`)

The if line joins a <- declaration and a ? set check. In plain words: "declare name from lookupName(), and run this block only if name is set." Inside the block, name is certain to hold a usable value, and you never need to check it again.

The example file Guards.ek9 shows both outcomes. Its program Greet has two guards: the first takes its value from lookupName(), which returns an unset String; the second from storedName(), which returns "Ada". Only the second block runs:

Hello, Ada

This is the heart of the chapter. The guard does not test whether name is true, or empty in some loose sense. It tests one thing: is the value set? That is the question ? has always asked.

Common confusion. if name <- ... does not mean "if the assignment worked" or "if the value is not zero". It is the set check, nothing more. If name is set, the block runs; if not, it is skipped.

One exception: a Boolean. If the guarded value is a Boolean, an if (or else if) guard runs its block only when the value is set and true: if allowed <- permitted() skips a false as it skips an unset value, because that is what anyone reading the line expects. A switch still asks only "is it set?", so its case false can be reached.

If you need an extra condition beyond "is it set?", add it after then:

minLen <- 2
if name <- storedName() then length name > minLen
  stdout.println(`Hello, ${name}`)

length name gives the length of the text. (The number 2 gets a name, minLen, as any number compared against must.) Now the block runs only when name is both set and longer than minLen characters; with "Ada" it prints Hello, Ada. The guard always comes first, and the then condition is checked only once the value is known to be safe to look at, so you can never ask the length of a value that is not there.

The same pattern, everywhere

The same declaration guard can start every choice and loop EK9 has. It always means the same thing — declare a value, and carry on only if it is set — though what follows it differs a little from one construct to the next.

In a while loop, the guard runs once, as the loop's setup, and a then clause gives the condition that keeps the loop going:

while iter <- lines.iterator() then iter.hasNext()
  line <- iter.next()
  stdout.println(line)

The guard iter <- lines.iterator() runs a single time: it declares iter (an iterator, which hands out a collection's items one at a time) and would skip the whole loop if that value came back unset. then iter.hasNext() is the question the loop asks each time round: is there another item?

A for loop uses the guard the same way, as a one-off setup step before the loop runs:

for prefix <- messagePrefix() then i in 1 ... 3
  stdout.println(`${prefix} ${i}`)

The same shape appears in switch and in try. There is really only one idea here — a set-checked declaration — shared by if, while, for, switch and try. In an if it is the test; in the loops it is a guarded setup, with the ongoing condition after then. You learn one safety pattern, not a different one for each construct.

Note. The variable a guard creates — name, iter, prefix — belongs only to its block; it cannot be used outside. So you can never reach for a guarded value whose set state has not been checked.

Setting a value only if it is missing

Sometimes you only want to supply a default when something is unset. That is the assign-if-unset operator, :=?:

name <- lookupName()
name :=? "Friend"
stdout.println(`Hello, ${name}`)

:=? reads as "set this only if it is currently unset". If lookupName() gave a real name, :=? leaves it alone; if it gave an unset value, :=? fills in "Friend". It behaves the same as the first version in this chapter, with one line in place of three.

Coalescing: choosing the set one

The tightest form is the coalescing operator, ?: (often called Elvis). Use it when you want "this value, or that one if the first is not set":

name <- lookupName() ?: "Friend"
stdout.println(`Hello, ${name}`)

lookupName() ?: "Friend" means "the result of lookupName() if it is set, otherwise "Friend"". The whole thing is an expression — it produces a value — so name can be declared from it directly.

The language choice in Speaking Every Language has the same shape. Greeting(requestedLang) is unset for a language you have not translated, so one line says what that chapter's ternary said:

greeter <- Greeting(requestedLang) ?: Greeting("en")

The coalescing operator is set-aware: it chooses a side by asking the set/unset question. EK9 has a small family of these set-aware choosers: ?:, and the comparison coalescing operators <? and >?, which pick the smaller or larger of two values safely (see below). The Operator Reference lists the whole family.

So the problem now has three answers.1 The example file Defaulting.ek9 holds each as its own program, and all three print Hello, Friend:

// Step 1 — plain
name <- lookupName()
if not name?
  name := "Friend"

// Step 2 — assign-if-unset
name <- lookupName()
name :=? "Friend"

// Step 3 — coalescing expression
name <- lookupName() ?: "Friend"

Read from top to bottom, they move from explicit to terse. Step 1 spells out every move, which makes it the clearest while the ideas are new. Step 3 says the same thing in one line, the form an experienced EK9 programmer writes for "cope with a value that might be missing". Step 2 sits between. Use whichever reads most clearly to you; as the set-aware forms become familiar, you will tend towards the terse end.

Now look at the three for reassignment. Step 1 declares name and then changes it: name := "Friend" is a second assignment. Step 3 works out the final value first and assigns it once. (Step 2's :=? sits between: it may fill an unset value, but never overwrite a set one.)

That difference matters. In Reading the Compiler's Guidance the compiler rejected a := inside a pure function, because a pure function may not reassign. So in pure code the Step 1 shape is not allowed, while the coalescing expression is: it changes nothing, it only works out a value. This is a first sight of a broader rule: EK9's expression forms (like ?:) are the preferred way to produce a value, because one expression replaces a declare-then-reassign sequence, and single assignment is what pure code needs.

The safe smallest and largest

Two more members of the family are used constantly: <? and >?, the safe smallest and safe largest. a <? b gives the lesser of the two, but if one side is unset it gives back the other; only if both are unset is the result unset. >? does the same for the greater value. If the two values are equal, both operators give back the left one.

Real data has gaps. Imagine asking three shops for a price, where one never replied:

#!ek9
defines module book.safeminmax

  defines program
    Show()
      stdout <- Stdout()

      // Three shops were asked for a price; one never replied (unset).
      shopA <- 12
      shopB <- Integer()
      shopC <- 9

      cheapest <- shopA <? shopB <? shopC
      dearest <- shopA >? shopB >? shopC

      stdout.println(`cheapest quote: ${cheapest}`)
      stdout.println(`dearest quote:  ${dearest}`)

      // Only when EVERY value is unset is the answer unset.
      noQuotes <- Integer() <? Integer()
      stdout.println(`no quotes at all -> set? ${noQuotes?}`)
cheapest quote: 9
dearest quote:  12
no quotes at all -> set? false

shopA <? shopB <? shopC works left to right, taking the smaller each time and passing over the shop that gave no quote, and arrives at the cheapest real price, 9. >? finds the dearest, 12. Here is the safe smallest of only two possibly-missing values, written by hand:

// The safe smallest of just two values, written by hand:
cheapest as Integer?
if shopA? and shopB?          // both shops replied — compare them
  if shopA <= shopB
    cheapest := shopA
  else
    cheapest := shopB
else if shopA?                // only A replied
  cheapest := shopA
else if shopB?                // only B replied
  cheapest := shopB
// neither shop replied — nothing to choose from

That is about ten lines to choose the smaller of two values, each branch a chance to slip. Each value you add doubles the set-and-unset combinations: two values give four cases, three give eight, four give sixteen. shopA <? shopB <? shopC stays one expression however many shops you add, and has no branches to get wrong. As long as one value is set, the answer is set; only when every value is unset is the result unset, which is the true answer: there was nothing to choose from.

Control flow that gives back a value

The coalescing operator is an expression: it produces a value. EK9's larger choices and loops can do the same. A for loop can hand back a result:

total <- for i in 1 ... 5
  <- rtn <- 0
  rtn += i
stdout.println(`Total is ${total}`)
Total is 15

The <- rtn <- 0 line declares the loop's result, starting at 0. Each pass adds i to it (+= adds to a value in place). When the loop finishes, total receives the final rtn. The loop has been used as an expression.

This fits code that works out a value without changing things elsewhere: pure code. A pure function may not reassign, so "declare a variable, then keep changing it" is not available to it. The expression forms — a coalescing ?:, a ternary, an expression switch or for — each let a choice hand back a value in one go, so you assign once.

EK9 offers expression versions of much of its control flow: switch, for, while and even try can each produce a value. One shape is deliberately missing: there is no if-expression. When you need one value or the other, the ternary (name? <- name : "Friend") says it more directly; when the choice is bigger, an expression switch does the job. These forms exist because a value produced by an expression can be assigned once, which pure code needs and which makes all code easier to read. Wherever you might declare a variable and reassign it inside branches, EK9 tends to give you a construct that hands the answer back:

greetingFor() as pure
  -> maybeName as String
  <- rtn as String: `Hello, ${maybeName ?: "Friend"}`

greetingFor is pure, takes a name that might be unset, and returns a greeting, computing the result in one expression and changing nothing. Called with "Ada" it gives Hello, Ada; called with an unset name, Hello, Friend. When code needs to change things outside itself, a plain statement is usually clearer. The second volume treats purity in full; for now, expressions and purity go together.

Why EK9 does this

The reason is evidence. A well-known and costly kind of bug is the one where a program assumes a value is there, and it is not. Most widely used languages have a null reference, a variable that refers to nothing at all, and reading one crashes the program. The person who introduced it later called it his "billion-dollar mistake".2

EK9 has no null keyword. It is designed to eliminate that bug, and the compiler checks every read. The tools in this chapter make checking a value's state so ordinary that it stops feeling like extra work: the block after a guard runs only when the value is set, :=? cannot overwrite a value you wanted to keep, and ?: with a set fallback always gives a set value.

Try it yourself

  1. Write a function pickColour() that returns a String which is sometimes set and sometimes unset. Use a guard (if colour <- pickColour()) to print the colour only when one was chosen.
  2. Rewrite the same logic with :=?, so an unset result falls back to "grey".
  3. Now do it in one line with ?:. Which of the three reads most clearly to you? That judgement is what this chapter is about.
  4. Take the plain if not x? example from the start of the chapter. Reduce it as far as you can while keeping it easy to read.

Recap

Every tool in this chapter is the ? set check in a new shape. The guard (if name <- ...) checks that a value is set and uses it in one step, and the same guard works across if, while, for, switch and try. Assign-if-unset (:=?) supplies a default only when a value is unset. Coalescing (?:) and the safe smallest and largest (<?, >?) choose a set value in a short expression. And control flow can be an expression, which suits pure code. Under all of it is one aim: no crash on a missing value. Next, Optional and Result: two containers that put "maybe a value" and "a value or an error" into the type itself.

  1. These are three alternatives; a real program uses one, not all three. Each also spells out the same fallback, "Friend". Write that literal three times in one real file and EK9 stops you: its repeated-literal rule (E11065) asks you to name it once as a constant. The example file does that — DefaultName <- "Friend", declared once and used in every version — so the value has a single home. ↩

  2. Tony Hoare, who introduced the null reference in 1965, called it his "billion-dollar mistake" in a 2009 talk (QCon London), estimating the cost of the resulting errors over the following decades. The Evidence: the research behind the rules has the background. ↩