Getting Started with EK9 · Chapter 11
Optional and Result
Collections named two small containers and promised to return to them. Optional and Result are
how an EK9 program says two things it needs to say all the time: "there might be a value here, or
there might not", and "this either worked, or it went wrong". Many languages have versions of these.
What EK9 adds is that the compiler will not let you reach a value you have not checked, and will not
let you quietly throw the answer away.
Optional: zero or one
An Optional holds either one value or none: the smallest collection, a box that holds at most one
thing and says when it is empty.
Use it when a function might not have an answer. Here is a phone book that looks up a number by name, where sometimes the name is not in it:
#!ek9 defines module book.phonebook defines function lookupNumber() as pure -> name as String <- rtn <- Optional() of String book <- {"Ada": "0100 111", "Grace": "0100 222"} if book contains name rtn: Optional(book.getOrDefault(name, String())) defines program Phone() stdout <- Stdout() ada <- lookupNumber("Ada") if ada? stdout.println(`Ada: ${ada.get()}`) zara <- lookupNumber("Zara") if zara? stdout.println(`Zara: ${zara.get()}`) else stdout.println("No number for Zara")
It prints:
Ada: 0100 111
No number for Zara
lookupNumber returns an Optional of String. If the name is in the book, it returns
Optional(theNumber), a box with a value. If not, it returns the empty Optional() of String it
started with. Either way the caller gets a real answer.
Now look at how the caller reads it. To take the value out you call .get(), but only after
checking that the box is not empty, with if ada?. Inside that block, and only there, ada.get() is
allowed. Leave out the check and the program does not compile.
Result: this worked, or it did not
An Optional says "a value, or nothing". Often you need to say why there is no value. That is a
Result: it carries either a success value or an error. Here is a division that refuses to divide by
zero:
#!ek9 defines module book.divide defines function safeDivide() as pure -> a as Integer b as Integer <- rtn <- Result() of (Integer, String) if b == 0 rtn: rtn.asError("cannot divide by zero") else rtn: rtn.asOk(a / b) defines program Div() stdout <- Stdout() good <- safeDivide(10, 2) if good.isOk() stdout.println(`Answer: ${good.ok()}`) bad <- safeDivide(10, 0) if bad.isError() stdout.println(`Cannot: ${bad.error()}`)
which prints:
Answer: 5
Cannot: cannot divide by zero
A Result of (Integer, String) holds an Integer on success or a String on failure. The return
line, <- rtn <- Result() of (Integer, String), declares the return value with an empty Result,
which the body then fills in; this is the short, inferred form from Functions, allowed because a new
Result() of (...) is a simple starting value. safeDivide fills it with asOk(...) or
asError(...). The caller asks which it got — isOk() or isError() — and only inside the matching
block may it read the matching value: ok() after isOk(), error() after isError().
The two outcomes are in the type. There is no error code in a separate variable for a caller to forget, and you cannot reach either outcome without checking first.
Note. The check must match the accessor. Checking
isError()and then readingok()is a compile error. You can read the success value only on the success path, and the error value only on the error path.
The compiler is watching
All of this rests on one rule: you cannot read a value you have not checked. The compiler keeps it. Try a shortcut:
maybe <- Optional("hi") stdout.println(maybe.get())
This does not compile (the whole program is uncheckedGet.ek9, in the error-examples folder):
EK9 : Error : E08030: 'get' on line 9 position 27: 'maybe as Optional of String': has not been checked before access
See: ek9 -h E08030
The compiler followed the code, found no if maybe? protecting the get(), and stopped. There is no
way to force the value out unchecked; the safe path is the only path.
Two more rules come from the same thinking. You cannot throw away a Result or Optional: if a
function hands one back, you must look at it (discardedResult.ek9):
EK9 : Error : E11052: 'work' on line 13 position 6: method returns Result or Optional but result is discarded - must always be checked
See: ek9 -h E11052
And inside a block that has checked a value, you cannot replace that value, because the check would
then no longer be true (staleCheck.ek9):
maybe <- Optional("hi") if maybe? maybe := Optional("bye") stdout.println(maybe.get())
EK9 : Error : E08040: ':=' on line 10 position 14: 'maybe as Optional of String': Reassignment/mutation within possible 'safe method access' scope is not allowed
See: ek9 -h E08040
Each is a small rule; together they close off a whole category of mistake.
Why it matters when the check happens
Optional and result types are common in modern languages.1 The difference is when a
mistake is caught. In EK9, a value read without a check and an error left unexamined each become a
program that does not build, with a message pointing at the exact spot. Catching it while you write
the code costs a moment; catching it in a running program costs far more. And there is no null to
fall back on (Guards and Safe Code told that story): Optional and Result are two of the tools EK9
offers in its place.
Try it yourself
- Add
"Steve"to the phone book, look him up, and print his number inside anif ... ?check. - Look up a name that is not there and handle the empty case with an
else. - In the division program, call
safeDivide(7, 0)and print the error. Then try to readgood.ok()without theif good.isOk()check, and read the compiler's error. - Change the last exercise to check
isError()but readok(), and see the compiler catch the mismatch.
Recap
EK9 has two containers for uncertainty. An Optional holds a value or nothing; you read it with
.get() only inside an if value? check. A Result holds a success or an error; you read
ok() only after isOk(), and error() only after isError(), with the check matching the
accessor. The compiler enforces all of it: unchecked access (E08030), a discarded result
(E11052), and a check made stale by reassignment (E08040) are compile errors.
Next come streams: a way to run a whole collection of values through a pipeline of small steps.
-
In Rust, an
OptionorResultalso provides.unwrap(), which crashes at run time if the value is missing. In Go, a function returns a value and an error side by side, but nothing forces the caller to look at the error. EK9 turns both of those mistakes into compile errors. ↩
In the EK9 reference: Exception Handling in EK9, EK9 Standard Types