Getting Started with EK9 · Chapter 4
Functions
So far, each program has been one block of instructions from top to bottom. Real programs do the same job in many places — squaring a number, greeting a person, checking a price — and copying the same lines again and again invites mistakes. A function lets you write a piece of work once, give it a name, and use it as often as you like.
You have already used one: stdout.println(...) calls a function. In this chapter you write
your own.
Your first function
Here is a function that squares a number, that is, multiplies it by itself.
#!ek9 defines module functions.demo defines function square() as pure -> n as Integer <- result as Integer: n * n defines program Demo() -> start as Integer stdout <- Stdout() stdout.println(`square of ${start} is ${square(start)}`)
Run it with a 5 and it prints:
square of 5 is 25
Look at the function on its own:
square() as pure -> n as Integer <- result as Integer: n * n
It has three parts, all built from pieces you have met. (The as pure on the first line is
optional and is explained in its own section below.)
square() names the function. The round brackets mark it as something you can call, like a
program.
-> n as Integer is the input, also called a parameter. The -> means "this comes in", as
it did for a program's command-line value in Making Decisions and Repeating Work. This function
is given one whole number, and inside the function that number is called n. You must use it:
declare an input and ignore it in the body, and EK9 will not build the function (E08091). An
unused input is nearly always a leftover or a slip.
<- result as Integer: n * n is the output, also called the return value. The <- here
means "this goes back out". The function returns one Integer, called result, and its value is
n * n.
The return value is a variable, like those in Values and the Three States: declared, given a
starting value, and handed back when the function ends. Here the starting value is the answer.
Sometimes it reads better to give the return value a plain starting value and settle it inside
the body, with an if, say. When that starting value is simple — a literal like 0, or a new
unset value like String() — you can let EK9 infer the type with <-, as for ordinary variables:
describe()
-> known as Boolean
<- result <- String() // start empty, fill it in below
if known
result: "known"
else
result: "unknown"
A computed value, worked out with an operator or another call like n * n, keeps the longer
as Type: form; the short <- is only for a simple starting value. The same short form returns
later, when Result and Optional are declared with an empty starting value.
Calling a function
To use a function, you call it by name and give it the value it asks for (an argument):
square(start)
If start is 5, then square(start) works out to 25. A call stands for the value it
produces, so you can put it inside a larger expression — here, inside a string:
stdout.println(`square of ${start} is ${square(start)}`)
That is the point of a function: you write the "how" once, and everywhere else you use the name.
Note. You cannot run a function on its own; it does something only when something else calls it. So far, that is a program: "run it" means run the program, which calls the function. The next chapter shows another way to exercise a single function — a test — and the habit of writing the test before the code.
More than one input
A function can take more than one value. List them in an indented block under a single ->:
add() as pure -> a as Integer b as Integer <- total as Integer: a + b
add takes two numbers, a and b, and returns their sum. You call it with two values:
add(2, 3) // works out to 5
Note. Write the
->once, then list the inputs under it. A separate->for each input is a compile error.
Functions can use other functions
A function's body is ordinary code, so it can call other functions, including your own. This one
adds up two squares by reusing square and add:
sumOfSquares() as pure -> a as Integer b as Integer <- total as Integer: add(square(a), square(b))
square(a) and square(b) are worked out first, and their results are handed to add. With a
of 5 and b of 3, that is 25 + 9, so sumOfSquares(5, 3) is 34. Small functions built
from smaller functions are how large programs stay understandable.
Sometimes there is nothing to return
Not every function gives a value back. Some do a job: print a message, save a file, start
something off. When a function has nothing to return, leave out the <- line:
#!ek9 defines module functions.noreturn defines function //No '<-' line: this function does its work and returns nothing. announce() -> name as String stdout <- Stdout() stdout.println(`Hello, ${name}`) defines program Demo() announce("Ada") announce("Grace")
announce has an input (->) but no output line, so it returns nothing. Run it and it prints:
Hello, Ada
Hello, Grace
With no value to use, you call such a function as a line on its own: announce("Ada"). There is
no keyword to write for "returns nothing"; leaving out the <- line says it.1
pure: a promise not to change things
as pure is a promise about change.
A pure function may declare as many variables as it likes; what it may not do is change one once it is set. It cannot reassign its variables, and it cannot alter the inputs it was given. The result it is building is the exception: filling that in is the point of the function.
Reassign an ordinary variable inside a pure function and EK9 stops you:
count <- 0 count := count + 1 // not allowed inside a pure function
The compiler refuses the second line and suggests :=?, the assign-if-unset operator, which
Guards and Safe Code introduces. square, add and sumOfSquares are pure with no effort:
each declares a result, works it out, and returns it.
pure does not forbid real work. A pure function can print (the printing operations are
themselves pure). It may only call other pure functions, so the promise holds all the way down.
EK9 uses the word in its own way.2 In mathematics, and in functional languages, a pure
function has no effect on the outside world at all: it may not print or touch a file, only
compute. EK9's pure is a rule about change, not about effects: do not reassign, do not
alter what you were given. Printing a line changes none of your program's own values, so EK9
allows it; reassigning a variable does, so EK9 forbids it. That is what lets pure be a promise
you keep in ordinary code, including code that writes to the screen.
Why does it matter? Nothing changes under you while you read a pure function. square(5) always
gives 25, because square uses only its input. (pure itself promises only that nothing is
reassigned or altered, not that every pure function gives the same answer each time.) Code you can
trust like that is code you can build on; the idea returns in later chapters.
A fair question. If you cannot reassign a variable, how do you work out a result that depends on a condition, or one built up in a loop? EK9's answer is to let a choice or a loop produce a value, which sets your result in one go. For a simple either/or there is a short ternary form; for a bigger choice, a
switchthat hands back a value; for a loop, aforthat hands back a value. They exist because a pure function needs them. Guards and Safe Code covers them.
require: saying what must be true first
Sometimes a function only makes sense if its input meets a condition. You state that condition with
require. It is a precondition: something that must be true before the work runs.3
halve() as pure -> n as Integer <- result as Integer? require n? result: n / 2
require n? says n must be set. If it is not, the program stops completely. It panics:
it halts with a message naming the broken requirement, and exits with a non-zero code. To see it,
here is a caller with a deliberate bug: it hands halve an unset Integer() (the file is
HalveNothing.ek9, with the same halve as above):
Broken()
stdout <- Stdout()
stdout.println(`half is ${halve(Integer())}`)
Running it prints:
Exception in thread "main" Panic: Requirement failed: `n?` at ./HalveNothing.ek9:10:7: Exit Code: 1
A non-zero exit code is how a program tells whatever ran it "I failed". This is a hard stop, not a
warning, and that is what you want, because of what a broken require means.
A require is a promise the caller must keep. If it is broken, a value arrived that never should
have: a bug in the code that called the function, not something a person using your program
did. Stopping loudly at the broken promise is far better than carrying on and producing a wrong
answer that is found days later.
So require is for things that should always be true. It is not for input you expect might
be wrong, such as someone mistyping their age or leaving an answer blank. That is normal, and you
handle it by checking the value (with ? and the tools in later chapters) and telling the person
what to fix. require says: "this can never happen, and if it does, there is a defect, so stop."
Note.
requireis not the same asassert, which the next chapter introduces.requireguards real code: it states what a function needs.assertbelongs to tests, where you check that code did the right thing.
One value out
A function returns at most one value: one (like square) or none (like announce). That is
deliberate. A call stands for one value, so you always know what you are getting.
To hand back several pieces of information at once, say a name and an age, you bundle them into one thing, called a record, and return that. Records are introduced in Records and Enumerations.
Try it yourself
- Write a function
cubethat returnsn * n * n. Call it from a program and print the cube of a number you pass in. - Write a function
biggerthat takes two numbers and returns the larger. (Hint: anifinside the function, setting the return value.) - Add
require n?to yourcubefunction. Does it still work for normal numbers? - Write two
->arrows, one per input, on a two-input function. Read the compiler's error, then change it to a single->with an indented list.
Recap
A function is a named piece of work you can use again and again. It takes inputs with ->,
returns at most one value with <-, and is called by name. Functions can call other functions,
which is how large programs are built from small, understandable pieces. as pure promises
that a function changes nothing except the result it is producing; it can still do work, even
print. require states a precondition, and stops the program if it is broken, because a
broken require means a bug in the calling code.
Next, you check that your functions do what you think. You will write your first test, and see why, in EK9, testing is part of writing code from the start.
-
Many languages have a word such as
voidfor "returns nothing". EK9 has no such keyword to write. ↩ -
Two familiar reference points frame where EK9 sits. Most mainstream languages — Java, Python, JavaScript, C# — have no
purekeyword: nothing stops a function reassigning its variables or changing shared state, and any discipline lives in convention and review. The functional languages that do enforce purity, such as Haskell, take the strict view: a pure function may perform no side effect at all, so even printing goes through special machinery. EK9 sits between the two: purity is checked by the compiler, as in Haskell, but scoped to change rather than all effects. Treat EK9's meaning as its own rather than mapping it onto one you met elsewhere. ↩ -
Preconditions and postconditions are long-standing ideas in software engineering. A precondition is what must be true before a piece of code runs; a postcondition is what it promises after. C. A. R. Hoare gave the formal basis in "An Axiomatic Basis for Computer Programming" (1969). Bertrand Meyer built it into a whole method, Design by Contract, in the Eiffel language (1986), described in his book Object-Oriented Software Construction (1988). A central rule of that method is the one used here: if a precondition is broken, the caller is at fault, so the program should fail fast rather than carry on.
requireis EK9's precondition. ↩
In the EK9 reference: Dynamic Functions in EK9, Functions in EK9