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 switch that hands back a value; for a loop, a for that 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. require is not the same as assert, which the next chapter introduces. require guards real code: it states what a function needs. assert belongs 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

  1. Write a function cube that returns n * n * n. Call it from a program and print the cube of a number you pass in.
  2. Write a function bigger that takes two numbers and returns the larger. (Hint: an if inside the function, setting the return value.)
  3. Add require n? to your cube function. Does it still work for normal numbers?
  4. 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.

  1. Many languages have a word such as void for "returns nothing". EK9 has no such keyword to write. ↩

  2. Two familiar reference points frame where EK9 sits. Most mainstream languages — Java, Python, JavaScript, C# — have no pure keyword: 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. ↩

  3. 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. require is EK9's precondition. ↩