← Contents · Runnable Specifications by Eric Normand · Work in progress · Comments
95
Chapter objectives
Understand how function signatures concisely encode
the intent of a use case.
Discover how total functions make your domain model
more robust.
Learn to specify requirements with functions you can’t
dene.
Once you know how to data model, you should forget about
it. Start with modeling the operations. The operations will
give you so much information about how to encode your
data, and since youre good at data modeling, you can do it
later.
In this chapter, we’re going to learn how to reason about
operations without knowing how they are implemented or
how the values they operate on are encoded.
95
Chapter 3
Operation Lens
Important terms
function signature
total and partial func-
tions
constructor, accessor,
mutation, and combining
functions
denition vs implementa-
tion
you’ll learn these
terms in this chapter
96 Chapter 3
all of these are mutation
functions, which we’ll dene in a
few pages
Megabuzz touchscreen register
Here is the touchscreen the cashier sees as they take an or-
der.
The current size and roast are outlined and can be changed
by touching the icon for the desired size or roast. The add-
ins have + and - buttons that the cashier presses to change
the count of each add-in.
If we squint, we can turn each one of the presses into an
operation. Here are the operations written out as function
signatures.
These four operations cover the things the cashier can do
on the above touchscreen when taking a coee order. And
we’ve written them as function signatures. It turns out that
function signatures are a great way to reason about opera-
tions. Lets take a closer look at them on the next page.
Super Raw
Soymilk Espresso
Almond
Chocolate
Hazelnut
Mega BurntGalactic Charcoal
+
+
+
+
+
2
1
0 0
0
-
- -
-
-
function setSize(coffee, size) //=> Coffee
function setRoast(coffee, roast) //=> Coffee
function increaseAddIn(coffee, addIn) //=> Coffee
function decreaseAddIn(coffee, addIn) //=> Coffee
97Operation Lens
Function signature
function addAddIn(coffee, addIn) //=> Coffee
The function signature is the part of the function without its
body. It species the meaning separate from implementa-
tion. Here is the syntax we’ll use in the book:
A function signature is the part of the function that tells us
how to call it. In JavaScript, it is the rst line of the function,
along with the return type. It consists of three parts:
1. The name
The compiler doesnt care about the functions name, but
it bridges the gap to human understanding. Choose names
that use domain terms so the function matches the re-
al-world processes they represent.
2. The arguments
The arguments names and types show us what inputs are
required to operate. Explicitly state the inputs instead of im-
plicitly obtaining them in the body.
3. The return type
The return type explicitly describes the operations result.
There should be no implicit outputs.
The function signature outlines all you need for later de-
nition, serving as a crucial preparatory step. However, lets
hold o on dening the function until were sure we need
it—there is no point in wasting eort prematurely.
The function signature is an abstract representation of
the fully dened function. We use it to work and reason at
the right level of abstraction, free from implementation de-
tails.
we will use the TypeScript
name but with different
syntaxt that ts on the
page
the return typethe arguments
argument names imply their type
(because JavaScript)
the function name
Function signatures are
a concise and precise
requirement
the right level of ab-
straction for reasoning
easy to implement later
98 Chapter 3
Constructor functions
Constructor functions are the simplest pattern of function.
They are also known as factories. They take some parame-
ters and return the value we are constructing.
function coffee(size, roast, addIns) //=> Coffee
4 Function Patterns
1. Constructor functions
2. Accessor functions
3. Mutation functions
4. Combining functions
Constructor functions are so simple, they oen look like a
waste. But they’re not a waste at this design stage, and they
are important also during implementation.
Theyre important during design because we dont yet
know how we will encode the data as a data structure. We
need some way of representing the things we know (the pa-
rameters—size, roast, addIns) and what those turn into (the
return typeCoee). The constructor signature stands in
for the data structure we will eventually use in the imple-
mentation.
Constructors are important during implementation be-
cause they can include validation of the arguments if we
want to check them at runtime. We can also use the function
as a rst-class function where we pass it to other functions.
Were not deciding yet whether the implementation will
have constructor functions—that’s for the implementation
phase, and we’re still rmly in specication. Just know that
they can be useful, even if your language has literal data
structures like JavaScript does.
Other examples of constructor functions
function raw() //=> Roast
function burnt() //=> Roast
function charcoal //=> Roast
function roast(name) //=> Roast
// ex: roast("burnt")
Here are two ways of encoding the constructors for Roast.
Both versions avoid prematurely implementing.
This way uses a constructor per roast alter-
native.
This way constructs a roast given a name.
99Operation Lens
Accessor functions
Accessor functions are a sinple pattern of function. They are
also known as queries. They take a value and answer a ques-
tion about it, either directly plucked from the data or calcu-
lated from it. Here is a calculated one:
function price(coffee) //=> number
function roast(coffee) //=> Roast
The important thing is that it is like asking a question of the
value. In this case: What is the price of this coee?
Here is an accessor function that simply plucks the data
right out of the data structure:
Q&A
Why are we dening a roast() function when the imple-
mentation is so trivial?
Its true: We can implement roast() like this:
function roast(coffee) { //=> Roast
return coffee.roast;
}
Instead of writing roa st(coffee), we can just write
coffee.roast. So why would we ever write this signature?
It’s a good question. Remember, in this book, we are try-
ing to separate specication from implementation. coee.
roast contains an implementation. roast(coffee), with
only the function signature, does not have an implementa-
tion. It is pure specication.
When we separate out specication from implementa-
tion, were much more free to make better decisions, and
hence design better soware. Were using the function sig-
natures right now to reason abstractly. When we implement
later, we very well might use the built-in coffee.roast.
4 Function Patterns
1. Constructor functions
2. Accessor functions
3. Mutation functions
4. Combining functions
100 Chapter 3
Mutation function
Another common pattern is the mutation function. Mutation
functions take an value, typically as the rst argument, and
return a modied copy of that value. We use them to encode
operations that change state over time.
Remember, were using a functional style, so we cant liter-
ally mutate the data representation of coee. The best we
can do is return a modied copy.
Its easy to see mutation functions because the function
returns the same type as one of its arguments—the one it is
modifying. Typically, we put that argument rst. Mutation
functions encode a discrete change from one valid state to
another.
function setSize(coffee, size) //=> Coffee
takes a coffee, returns a modied
version of that coffee
the rest of the arguments are
parameters for the mutation operation
Mutation functions
encode operations that
modify values
take a value as rst ar-
gument
return a modied copy
of that value
take parameters for the
operation aer the val-
ue to mutate
encode a discrete
change from one valid
state to another
4 Function Patterns
1. Constructor functions
2. Accessor functions
3. Mutation functions
4. Combining functions
mutation functions are
more complex than
accessor functions
101Operation Lens
Combining function
The nal function pattern well talk about is the combining
function. Combining functions are the most complex of the
patterns weve seen so far. They take two (or more) argu-
ments of the same type, combine them, and return the re-
sult.
This function takes two values of the same type (AddIns)
and combines them into one value. We know a lot of opera-
tions like this already, such as
+, *, -, /
but also string concatenation and merging hash maps.
Combining functions tend to be more complex than mu-
tation function because combining two values is inherently
complex.
function mergeAddIns(addInsA, addInsB) //=> AddIns
4 Function Patterns
1. Constructor functions
2. Accessor functions
3. Mutation functions
4. Combining functions
takes two add-ins and
returns add-ins
combining functions are
the more complex of
the four
102 Chapter 3
Operation complexity
We should start by dening the complex functions because
in the long run it makes the work of design easier. The down-
side of complex functions is that they are harder to dene.
However, complex functions constrain the data structure
more. Those constraints enumerate the requirements of our
data structures, which helps eliminate data encodings that
won’t work. That makes it easier to choose a data structure.
Here is a diagram showing the relative complexity of the
three patterns of functions we’ve seen.
Starting a design with the simplest functions is common,
but it can be problematic. Oen, the simplest functions are
the accessor functions, such as getters, which are easy to
implement. However, if you start with the easy tasks, you
may discover later that the data encoding you chose is un-
suitable, and essential data may be missing. This can lead to
a messy implementation, and you may have to scrap much
of your work.
Thats why I recommend starting with combining func-
tions and nding a straightforward way to dene them.
This will inform your choice of data encoding. The simple
stu will always be easy, so you can put those o until last.
Lets look at an example on the next page.
constructor
functions
accessor
functions
mutation
functions
combining
functions
simple
complex
103Operation Lens
Worked example: Contact database
Lets work through an example of how starting with acces-
sors is a bad idea. We’ll develop a simple contacts app for
MegaBuzz’s sales reps to keep track of their contacts.
Here are the requirements: we need to be able to create
new contacts, read and modif y the contact s name and phone
number, and sync contacts across devices. Let’s break these
down into signatures. For simplicity’s sake we’re only going
to have one contact. And we’re not modeling networking or
anything like that. Just the basic contact with its operations.
function createContact(name, phone) //=> Contact
function getName(contact) //=> string
function getPhone(contact) //=> string
function setName(contact, name) //=> Contact
function setPhone(contact, phone) //=> Contact
function syncContacts(contactA, contactB) //=> Contact
constructor
accessors
mutation
combining
Let’s do the common practice (but not the practice I recom-
mend) of starting with designing/implementing the simple
functions, the constructor and the accessors. Because they
can’t be dened in terms of other operations, we have to im-
plement them, which means deciding on a data representa-
tion.
104 Chapter 3
The mutation functions are also easy:
We’ll dene it on the next page.
function setName(contact, name) { //=> Contact
return update(contact, 'name', () => name);
}
function setPhone(contact, phone) { //=> Contact
return update(contact, 'phone', () => phone);
}
function syncContacts(contactA, contactB) //=> Contact
We know the data represenation needs to store a name
(string) and a phone number (string), we classify that as a
combination. Without any more information, any encoding
of a combination will work, so we choose one of the combi-
nations we are familiar with. For this example, let’s choose
a JS Object:
function createContact(name, phone) { //=> Contact
return { name, phone };
}
function getName(contact) { //=> string
return contact.name;
}
function getPhone(contact) { //=> string
return contact.phone;
}
At this point, everything we’ve done has felt like super pro-
ductive work, but thats misleading. Even though we have
one function lethe combining functionthat one func-
tion is going to cause us to throw away all the implementa-
tion code.
The combining function we have le is this one:
105Operation Lens
Now we are in the home stretch! We have one function le.
syncContacts() takes two contacts that were saved on dif-
ferent devices, and returns a contact with the most recent
version of all of the data.
For instance, lets say I have my contacts on my laptop
and on my phone. While I’m walking in the park, by chance
I meet my friend Patricia feeding the ducks. She tells me she
has a new phone number. I type it into the app on my phone.
When I get back home, I expect that my laptop also has her
new number.
{
name: "Patricia",
phone: "444-4444"
}
{
name: "Patricia",
phone: "444-4444"
}
{
name: "Patricia",
phone: "444-4444"
}
{
name: "Patricia",
phone: "222-2222"
}
{
name: "Patricia",
phone: "222-2222"
}
old number
new number
syncContacts()
setPhone(patricia, "444-4444")
Note that our function doesn’t have to do all of the network
communication. Something else will do that. We just need
to dene the function that will decide the newest version of
the contact given two current versions.
But we cant dene that function. How can we choose
between the two phone numbers? We dont have any infor-
mation about which phone number is newer, or about how
they were changed, or anything. We just know the phone
number. We’re at a dead end.
106 Chapter 3
How did we get here? By starting with the easiest func-
tion to implement, and implementing them in the simplest
way, we arrived at a point where we could not continue.
syncContacts() is complicated and needs more informa-
tion. We need to throw away all of our work because the en-
coding needs to change.
What we need is some way to record the time the change
was made. That way, we know which changes are newest.
Once we have that, we can compare them. Let’s assume we
have it. We dont need to know how it is encoded, just that
we can get it with some accessors.
function syncContacts(contactA, contactB) { //=> Contact
let name, nameTime;
if(getNameTime(contactA) > getNameTime(contactB)) {
name = getName(contactA);
nameTime = getNameTime(contactA);
} else {
name = getName(contactB);
nameTime = getNameTime(contactB);
}
let phone, phoneTime;
if(getPhoneTime(contactA) > getPhoneTime(contactB)) {
phone = getPhone(contactA);
phoneTime = getPhoneTime(contactB);
} else {
phone = getPhone(contactA);
phoneTime = getPhoneTime(contactB);
}
return createContact(name, nameTime, phone, phoneTime);
}
The logic is not hard. We just need to know the time each
value was changed to know which one to pick. Dening the
complex function rst gave us new information required for
our design. Let’s look at how our design changes on the next
page.
Note
This is one possible deni-
tion. There’s a lot of possi-
ble improvement. You’re
welcome to redesign it, but
I won’t do it here. The point
has been made.
107Operation Lens
function getNameTime(contact) //=> Date
function getPhoneTime(contact) //=> Date
Dening synContacts() (previous page) requires two new
accessors:
We need to record those when we mutate the Contact, so the
mutator functions need new arguments:
We have to pass in the time because getting the system time
is an action (a side-eect) and so its an implicit input. We
want to encode our operations as calculations (pure func-
tions).
The constructor needs to change to record the time, too:
function setName(contact, time, name) //=> Contact
function setPhone(contact, time, phone) //=> Contact
function createContact(name, nameTime, phone, phoneTime) //=> Contact
Note that we cannot dene any of these functions in terms
of the others. They need implementation details (the details
of the data encoding). But syncContacts() was dened only
in terms of these. Yet another reason to dene it rst.
Well see this contacts example again, and the interesting
syncContacts() function, when we talk about the compo-
sition lens. This function is chock full of interesting prop-
erties.
108 Chapter 3
Q&A
What’s the dierence between a denition and an implemen-
tation?
Thats a tough question. People in our industry do not typi-
cally distinguish between them, but we should in this book.
A denition expresses the meaning of a domain concept in
terms of the meanings of other domain concepts. A deni-
tion relates the meanings together. Here’s an example:
function coffeePrice(coffee) { //=> number
return sizePrice(size(coffee)) +
addInCollectionPrice(addIns(coffee));
}
An implementation, on the other hand, is any code that gives
the right answer, regardless of how. An implementation
might rely on the details of a data structure or other ideas
that are not from the domain. Heres an example implemen-
tation that assumes the encoding of the coee and that add-
ins are in a Map:
function coffeePrice(coffee) { //=> number
let price = sizePrice(coffee.size));
for(const [addIn, quantity] of coffee.addIns)
price += addInPrice(addIn) * quantity;
return price;
}
A denition is a kind of implementation, since it is code that
gets the right answer. But a denition denitely cares about
how it is implemented.
Why distinguish the two?
Our industry does not make this distinction, but it is essen-
tial for the skills in this book.
We want to separate thinking about the specication
from thinking about the implementation. When we dene
a function in terms of other domain operations, we can de-
sign in the realm of domain meanings.
109Operation Lens
Dening addInCollectionPrice()
Here’s another version using a higher-order function:
eachAddInQuantity() will call func for each add-in and
its quantity, then return an array of the result of each call.
We will allow it to be a linear operation. We could dene
howManyAddIns() in terms of it:
function addInCollectionPrice(addIns) { //=> number
return sum(eachAddInQuantity(addIns, (addIn, quantity) => (
addInPrice(addIn) * quantity
)));
}
function howManyAddIns(coffee, addIn) { //=> number
return sum(eachAddInQuantity(addIns(coffee), (a, q) => (
a === addIn ? q : 0
)));
}
Existing functions:
function addIns(coffee) //=> AddInCollection
function addInPrice(addIn) //=> number
New functions:
function sum(numbers) //=> number
function eachAddInQuantity(
addIns,
func /* AddIn, number -> T */
) //=> [T]
Some functions are dened in terms of others. Some are as-
sumed to exist (and they will when we implement them). It’s
a bit like Euclid’s Elements, where some concepts are simply
given with no proof. They are assumed to exist. Then other
things are proven in terms of those. We will call those prem-
ises.
Or we could leave howManyAddIns() as a premise.
110 Chapter 3
Total function
Total functions is an important idea from functional pro-
gramming that will help us encode our models better.
A total function has a valid answer for every combina-
tion of valid arguments. The opposite of a total function is a
partial function.
Division is a famous partial function. Most numbers give
a good answer, but if we divide by zero, the answer is unde-
ned. Here is an implementation of a divide() function:
This function signature implies that we can pass it any two
numbers and get a number. But it’s not completely true. If
the divisor is zero, this function will throw an error, mak-
ing divide() a partial function.
Partial functions can do a number of things when they
don’t have a valid answer:
1. Crash
2. Throw an exception
3. Return the wrong answer
4. Do whatever (undened behavior)
You need to decide what you want to happen for your code. A
useful exercise is to list the pros and cons of each.
But total functions don’t do that. They work correctly for
all valid arguments. In a sense, we could say that total func-
tions say what they do and do what they say.
We prefer total functions because they are a more sta-
ble foundation to build on. When we write functions, those
functions will be called by other functions, which will be
called in turn. As our code grows, those layers get higher
and higher.
If the functions at the bottom have special values we cant
use, it becomes harder to reason about the functions in the
top layers. Their behavior is more complex and harder to
keep in our heads. Total functions are simpler to work with.
Luckily, we can make any partial function total.
function divide(dividend, divisor) { //=> number
return dividend/divisor;
}
111Operation Lens
Partial to total: Augment the return
Since it is easier to reason about total functions than partial
functions, we want to be able to convert a partial function
to a total one. There are three ways to make a partial func-
tion total:
1. Augment the return
2. Restrict the arguments
3. Change the meaning of the function
These three ways correspond to the three parts of the func-
tion signature. Lets go over each of them for division.
First, recall the standard denition of divide():
function divide(dividend, divisor) //=> number | undened
function divide(dividend, divisor) //=> number
function divide(dividend, divisor) { //=> number | undened
if(divisor === 0)
returnundened;
else
return dividend/divisor;
}
We can augment the return type with another case. We
change the function signature’s return type to return either
a number or undened:
This method changes the return type, therefore changing
the promise the signature is making. But notice what hap-
pened: Now the caller has to work with a more complicat-
ed return value. The return value could either be a number
(great!) or undened (which well have to check for). Aug-
menting the return changes the burden from one of catch-
ing errors to handling dierent kinds of return values. The
burden is still on the caller.
We can implement it like this:
112 Chapter 3
Another way to make a partial function total is to restrict
the argument types. Lets restrict divisor, the problematic
argument.
One way to restrict it to make a new type of number that
can never be zero. Well call it NonZeroNumber. It will need a
constructor and an accessor to get the number value:
function divide(dividend, nonZeroDivisor) { //=> number
return dividend/asNumber(nonZeroDivisor);
}
type NonZeroNumber = { number: number };
function nonZeroNumber(number) { //=> NonZeroNumber | null
return number === 0 ? null : { number };
}
function asNumber(nonZeroNumber) { //=> number
return nonZeroNumber.number;
}
We can now dene divide() like this:
This is probably close to what mathematicians mean when
they say “division by zero is undened. Basically: Make
sure you never divide by zero.
How do we get a NonZeroNumber? Well, divide() doesn’t
need to care. But, since were curious, lets implement ev-
erything, just to know we can:
If we try to construct an instance of this type using zero, it will
return null. But if we have an instance of NonZeroNumber,
we are sure that divide() will not blow up.
Notice the tradeo: Somewhere before divide() is called,
something has to check if its zero (or check for null from the
constructor). Restricting the arguments changes the bur-
den from catching errors from the function call to ensuring
the arguments are valid before calling the function.
Partial to total: Restrict the arguments
function nonZeroNumber(number) //=> NonZeroNumber | null
function asNumber(nonZeroNumber) //=> number
constructor
the caller of the
constructor has to
check for null
113Operation Lens
The last way to make a partial function total is to change the
meaning. This corresponds to the function name part of the
function signature.
Unfortunately, we cant really change the meaning of di-
vision, so its a bad example. But I can share another one.
In JavaScript, there is an operator called delete that re-
moves a key/value from an object based on the key. Simi-
lar constructs exist in other languages. Some languages
(but not JavaScript) throw an error if you try to remove a
key that does not exist. Throwing an error in those cases is
a choice the designers made. Here is a signature for a func-
tional-style delete operation that throws errors if the key
does not exist.
function removeKey(object, key) //=> Object; throws error
function removeKey(object, key) //=> Object
One can see that the meaning of the name in English does
correspond to the behavior: How can you remove a key that
does not exist? If it doesn’t exist, removing it is meaningless,
so an error is thrown. It is a partial function.
We can make it total by changing the meaning of the
function. Instead of taking “remove key” literally, we could
interpret it as “ensure the key is not in the object.” If its not
in the object, there’s no work to do, so we can return the ob-
ject unchanged.
With that interpretation, the signature becomes:
I’ll leave implementing it (in a functional style) up to you.
Though at this point we are not concerned with production
implementations, writing a prototype implementation can
prove that it is possible.
Notice, however, that changing the meaning is the only
method that removes a burden. There are no errors to catch,
no extra return types to check for, and no conditions on the
arguments. It is not always possible to do—like in the case
of division—but when it is, we prefer it for making our code
simpler.
Partial to total: Change the meaning
Exercise