Getting Started with EK9 · Chapter 13
Two Ways to Build: Data, Functions and Objects
So far you have built programs from the types EK9 provides — String, Money, List and the rest —
joined together with functions. From here on you make your own types, named in the words of the
problem you are solving: a Point, a Score, a Player. Before the details, this chapter sets out
the choice you face when you do, because there is more than one good way to build, and EK9 lets you
use them together.
Two traditions
Programming has two long-standing ways of organising a program.
The first keeps data and behaviour apart. You describe your data as a plain bundle of named
fields — a point is an x and a y — and write separate functions that take that data and do
things with it. The data is passive; the functions do the work. This is the tradition of the C
language and its struct, and it is simple and direct.
The second bundles data and behaviour together. A Counter is not just a number; it is a number
and the operations that belong to it — increment, current — packaged as one thing, an
object, that looks after itself. This is the tradition of Smalltalk, and later C++, Java and many
others.
Both have built large, successful systems. Neither is "right". They suit different problems, and often different parts of the same problem.
EK9 does not make you choose
Many languages pick a side. C has structs and functions but no objects. Java made almost everything an object and, for a long time, had no free-standing functions at all. In either, you shape your problem to fit the language.
In EK9, data, behaviour and objects are equal partners, and you pick whichever fits. There are three building blocks, and you have already glimpsed each of them.
A record is data — a bundle of named fields:
Point
x as Float: 0.0
y as Float: 0.0
default operator ?
A function is behaviour on its own. It takes something in, hands something back, and holds no
state, as in the Functions chapter:
add() as pure -> a as Integer b as Integer <- rtn as Integer: a + b
A class is an object: data and the behaviour that belongs with it:
Counter
count as Integer: 0
increment()
count += 1
current() as pure
<- rtn <- Integer(count)
default operator ?
current hands back Integer(count) — a copy of the count, not the field itself.1
None of the three is second-class. A record is not a lesser class, and a function does not have to
live inside an object. Each is a complete way to build, and one program can use all three.
Which should you reach for?
A rough guide, which the next chapters make precise:
- When you mostly have data to carry around — a point, a row from a file, a message — use a
record, and writefunctions that work on it. This is often the right choice. - When a thing has data that must be looked after — kept valid, changed only in controlled ways —
use a
class, so the data and the rules that protect it live together. - When you have behaviour with no state — a calculation, a step in a stream pipeline — a
functionis all you need.
You do not have to decide perfectly. A program can mix all three, and you can change your mind later.
Protecting data without freezing it
If a record's fields can be read and written by any code, what stops a distant piece of code from changing one when it should not?
Many languages answer by freezing the data: an "immutable" record that can never be changed. EK9 does
not. It controls the code instead. A function or method marked pure — the word from the
Functions chapter — may not change anything it is handed. Pass a record through pure code and it
comes back exactly as it went in.
So the record stays plain and open, and the protection sits on the behaviour, not on the data. A
value that is frozen in one part of a program and changeable in another brings complications of its
own; putting the rule on the code avoids them. pure gets its full treatment in the second volume.
Note.
recordandclassare both kinds of aggregate — a construct that defines a type with members of its own: fields, methods, operators. (A function is not an aggregate; of the three building blocks, onlyrecordandclassare.) Aggregates share a few rules (every aggregate that holds fields defines a?, so it can answer "am I set?"), which the next chapter introduces. Later you will meet three more aggregates: thetrait, for behaviour shared across types (Traits and Composition), thecomponent, for putting a whole subsystem behind a simple interface (Components), and, in the second volume, theservice, for web services. The Constructs at a Glance appendix summarises them all on one page.
Try it yourself
- Write a
purefunctiondistanceFromOriginthat takes aPointand returns how far it is from(0, 0). (sqrtwritten in front of a number gives its square root.) For the point(3.0, 4.0)it should give5.0. - Give
Counterareset()method that puts the count back to0, and print the count before and after. - For each of these, decide whether a record, a function or a class fits best, and say why in one line: a name and an email address; a count of page visits; turning Celsius into Fahrenheit.
Recap
There are three ways to build: data, behaviour and objects. Two traditions lie behind
them — keeping data and functions apart, and bundling them into objects — and EK9 treats
record, function and class as equal partners you can mix freely. A record's data
stays open; pure is how EK9 protects it. Next: records and enumerations — how to declare your own
types, give them fields, and give them a little behaviour.
-
Handing the field back directly would give the caller a reference to the counter's own state, which they could then use to change it. Returning a copy,
Integer(count), keeps the counter in charge of its value. The Classes chapter explains this in full; the examples adopt the habit from the start. ↩
In the EK9 reference: Classes in EK9, EK9 Design Patterns, Design note: records are anemic by design, Design note: folding design patterns into the language, Records in EK9