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 aStringwhich 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. Ifnameis set, the block runs; if not, it is skipped.
One exception: a
Boolean. If the guarded value is aBoolean, anif(orelse if) guard runs its block only when the value is set andtrue:if allowed <- permitted()skips afalseas it skips an unset value, because that is what anyone reading the line expects. Aswitchstill asks only "is it set?", so itscase falsecan 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
- Write a function
pickColour()that returns aStringwhich is sometimes set and sometimes unset. Use a guard (if colour <- pickColour()) to print the colour only when one was chosen. - Rewrite the same logic with
:=?, so an unset result falls back to"grey". - Now do it in one line with
?:. Which of the three reads most clearly to you? That judgement is what this chapter is about. - 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.
-
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. ↩ -
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. ↩
In the EK9 reference: Flow Control in EK9