← 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
114 Chapter 3
The options for dening setSize()
We should list the options we have for dening our opera-
tions, then eva luate those opt ions. Let s sta rt w it h setSize().
Although setSize() may appear straightforward, there are
real choices to be made, and we should take the time to eval-
uate them. Remember: The quality of your design is propor-
tional to how many possibilities you consider. Practicing
the evaluation now will make it faster to do it in the wild.
Partial function
We could encode setSize() as a partial function. That is,
we could throw an error if we try to set the size to what it
already is. For instance, if we set the size of a super coee to
super, that would throw an error.
Augment the return type
We could augment the return type with a value that indi-
cates that nothing was changed. We could use any value
thats not a Coffee. Here, well use false to indicate that
nothing was changed.
Restrict the argument types
We could restrict arguments to values that are legal. Name-
ly, the getSize(coffee) !== size.
Change the meaning
Finally, we could change the meaning from “change the size
to x to “ensure the size is x. If it’s already x, there’s nothing
to do.
function setSize(coffee, size) //=> Coffee; throws error
function setSize(coffee, size) //=> Coffee | false
function setSize(coffee, size) //=> Coffee
// getSize(coffee) !== size
function setSize(coffee, size) //=> Coffee
Ill spill the beans: The last one is the best. But why? Were
trying to balance two of three factors.
115Operation Lens
The three factors
Domain
Computer
Programmers
There are three factors any program needs to meet:
1. Domain - t
2. Programmers - elegance
3. Computer - eciency
Domain
The domain dictates the behavior of our soware. We’ve al-
ready seen how an encoding can be evaluated against the
domain (by way of the model) using t. The domain is the
most important force. Without the domain, our soware
doesn’t have a job. And without good t, it doesn’t do a good
job. The domain’s stakeholders are users and customers.
Programmers
We write code mostly for programmers to read and modify,
only secondarily for computers to run. Computers can han-
dle incredibly complex code, like deeply nested condition-
als, unintelligible algorithms, etc. They don’t mind.
But people do. We need to keep it simple, straightforward,
andquite franklybeautiful. And we want it to use exist-
ing constructs.
We work all day in the code. We want to read it and mod-
ify it without unnecessary struggle. We have a wonderful
aesthetic sense of elegance, which is the same principle that
guides scientic theories toward truth. We should seek that
truth in our code, too.
Computer
Eventually, our soware will need to run on a computer.
It needs to be ecient enough to do its job in a timely and
cost-eective manner. We want to consider eciency last,
aer we are sure we have the behavior we need. And, hope-
fully, we won’t need to optimize much at all.
low
priority
high
from SICP
116 Chapter 3
List the operations
At the beginning of this chapter, we listed four operations
that we extracted from the touchscreen interface the ca-
shier uses. We used function signatures to encode the use
cases of the domain.
However, there are more use cases that dont appear on
that touchscreen. The more use cases we can gather up
front—before implementation—the more information we
will have to make better design decisions.
Besides looking at user interfaces, another way to gather
use cases is to talk to the users. Some of the users of this do-
main are the cashier, the barista (the coee maker), and the
marketers.
Cashier
The cashier says that they need a way to know the price of
a coee so that they can tell the customer how much to pay.
We can turn that into a function signature:
Ba rista
When making the coee, the barista needs to know how
many of each add-in to put. So when they’re at the soy milk
station, they can see 3 soy shots and when theyre at the
espresso machine, they see “0 soy shots.As a function sig-
nature, this use case looks like:
Marketer
The marketers do research into the popularity of dier-
ent add-ins. They want to know what percentage of coees
contain almond syrup, or how many coees have espresso
shots. They don’t care how many shots of almond syrup a
coee has, just yes or no, does it contain it:
function howManyAddIns(coffee, addIn) //=> number
function hasAddIn(coffee, addIn) //=> boolean
There will be many more operations from all of the users
of your soware. The more operations you can collect, the
more holistic your view will be, and therefore the easier it
will be to make good decisions.
function coffeePrice(coffee) //=> number
117Operation Lens
Operations shed light on data encoding
In the Data Lens chapters, we had three ways we could en-
code the add-ins collection:
1. Array
2. JS Object
3. Map
We eliminated Set because it couldn’t handle duplicates, so
it wouldn’t be able to represent all states from the domain.
But the three that remained seemed equally good. They
all had their meaningless states which we could eliminate
with normalization and validation functions. The operation
lens gives us a dierent angle with which to view them. This
will help us rank them and choose the best one.
We will take the three operations we learned about from
the users of our soware and implement each one. Then
we’ll compare them against our three criteria, t, elegance,
and eciency.
Set
new Set(["almond",
"espresso"])
JS Object
{"almond": 1,
"soy" : 2}
Array
["soy", "hazelnut"]
Map
new Map([["soy", 1]])
0
all
["soy", "almond"]
["almond", "soy"]
...
0
some
new Set(["soy, "soy"])
new Set(["almond", "almond"])
...
all
0
{"soy": 0}
{"soy": -1}
{"dfs": 3}
...
all
0
new Map([["soy", -1]])
...
118 Chapter 3
How does calculating the price aect data encoding?
Our cashiers need to know the price of a coee to collect
money from the customer.
function coffeePrice(coffee) //=> number
function coffeePrice(coffee) { //=> number
return sizePrice(size(coffee)) +
addInCollectionPrice(addIns(coffee));
}
At MegaBuzz, the cost of a coee is based on the size and the
add-ins, as follows:
Notice that we’re denining price() in term of functions we
don’t have yet. We need to add their signatures to our list of
operations:
function size(coffee) //=> Size
function sizePrice(Size) //=> number
function addIns(coffee) //=> AddIns
function addInCollectionPrice(addIns) //=> number
We will implement addInCollectionPrice() since it takes
AddIns as an argument. Although we are still focused on
specication instead of implementation, it can be valuable
to probe the implementation to understand how the price
calculation aects the add-ins collection encoding. We want
to evaluate our possible encodings, so we need to imple-
ment it for each. We are looking for perfect t and the most
elegant solution.
when we dene a
domain operation, we
do so in terms of other
domain operations
Goal
Learn how coffeePrice() constrains add-ins encoding.
Plan
nd functions with AddInCollection arguments
implement them for each possible encoding
choose the most elegant denition
Lets see the three implementations on the next page.
119Operation Lens
Implementing addInCollectionPrice() for 3 encodings
JS Object
function addInCollectionPrice(addIns) { //=> number
return (
Object.entries(addIns)
.map(([addIn, count]) => addInPrice(addIn) * count)
.reduce((a, b) => a + b, 0)
);
}
Map
function addInCollectionPrice(addIns) { //=> number
return (
addIns.entries()
.map(([addIn, count]) => addInPrice(addIn) * count)
.reduce((a, b) => a + b, 0)
);
}
Now we can evaluate them against our three factors.
The array encoding is a clear winner in this round.
Array
function addInCollectionPrice(addIns) { //=> number
return (
addIns.map(addInPrice)
.reduce((a, b) => a + b, 0)
);
}
Fit
All three implement the de-
sired operation correctly
and with perfect t.
Elegance
The three implementations
share a very similar struc-
ture, but the Array imple-
mentation is a clear win for
elegance.
Eiciency
Of the three implementa-
tions, array does slightly
better since it does not cre-
ate the key/value entry pairs
the others do.
function addInPrice(addIn) //=> number
We have a new operation
to add to our list:
120 Chapter 3
Now we can evaluate them against our three criteria.
Fit
All three implement the desired operation correctly and
with perfect t.
Elegance
JS Object and Map are more elegant. Array is less concise.
Eiciency
JS Object and Map are more ecient. The array implementa-
tion needs to do a linear scan over the array. The other two
are done in constant time.
How does counting an add-in aect data encoding?
The barista needs to know how many add-ins to put at the
station they are at:
function howManyAddIns(coffee, addIn) //=> number
Array
function howManyAddIns(coffee, addIn) { //=> number
returnaddIns(coffee).lter(a=>a===addIn).length;
}
JS Object
function howManyAddIns(coffee, addIn) { //=> number
return addIns(coffee)[addIn] || 0;
}
Map
function howManyAddIns(coffee, addIn) { //=> number
return addIns(coffee).get(addIn) || 0;
}
Lets implement it for each possible encoding of
AddInCollection.
121Operation Lens
How does checking an add-in aect data encoding?
The marketer needs to know if a coee has a given add-in:
function hasAddIn(coffee, addIn) //=> boolean
function hasAddIn(coffee, addIn) { //=> boolean
return howManyAddIns(coffee, addIn) > 0;
}
Well, heres the surprising thing: Once we have
howManyAddIns(), we can dene hasAddIn() in terms of it:
This denition is independent of how AddInCollection is
encoded!
However, we should consider one thing: If we have one
function dened in terms of howManyAddIns(), will we have
others? Are we burying a call to a function at the bottom of
a call stack, obfuscating its actual importance?
122 Chapter 3
Using the spec to dene the requirements of our encoding
One advantage of staying in the specication side (as op-
posed to the implementation side) is that you can elaborate
the requirements to a high level of precision. In fact, you
can exactly specify the requirements of the encoding for a
particular concept using only operations. Lets see how to
do that now for the AddInCollection.
Previously, we dened coffeePrice() like this:
Its a true denition as opposed to an implementation be-
cause the meaning of the function is stated in terms of
the meanings of other functions. But we implemented
addInCollectionPrice() three dierent ways. Here is the
map one:
Notice how the implementation details, especially the en-
coding of the AddInCollection, really complicated the
function. But it doesnt have to be this way. We were pur-
posefully probing the implementation to understand how it
would change based on the encoding.
But theres another skill we can employ: That of incre-
mentally rening a denition until we cant dene anything
without knowing the implementation details. When we do
that, well get a precise specication of the requirements for
our data encoding.
Lets do it for addInCollectionPrice().
function coffeePrice(coffee) { //=> number
return sizePrice(size(coffee)) +
addInCollectionPrice(addIns(coffee));
}
function addInCollectionPrice(addIns) { //=> number
return (
addIns.entries()
.map(([addIn, count]) => addInPrice(addIn) * count)
.reduce((a, b) => a + b, 0)
);
}
123Operation Lens
Dening addInCollectionPrice()
The rst thing we need to do dene our function in terms of
other functions. Some of those functions will already be on
our list and some of them we’ll have to add. There are many
ways to dene it. Well have to evaluate all of the ways to
nd the best one. Here’s one way:
function addInCollectionPrice(addIns) { //=> number
return sum(eachAddIn(addIns, addInPrice));
}
function howManyAddIns(coffee, addIn) { //=> number
return sum(eachAddIn(addIns(coffee), a => a === addIn ? 1 : 0));
}
Existing functions:
function addIns(coffee) //=> AddInCollection
function addInPrice(addIn) //=> number
New functions:
function sum(numbers) //=> number
function eachAddIn(addIns, func/* AddIn -> T */) //=> [T]
sum() adds up numbers. eachAddIn() is a higher-order func-
tion that calls func() on each individual add-in, returning
an array of the results. Now, this gives me an idea! We can
dene howManyAddIns() in terms of these as well:
This will work, it has perfect t, but its not very elegant.
We’re representing a match of the add-in with a 1 or 0. We’re
reinventing Booleans but with numbers so we can add them
up. We’re already using Booleans elsewhere, so now we’ve
got two representations of them. It will work but its too
clever to be elegant.
124 Chapter 3
Conclusion
In this chapter, we saw a practical way to work with function
signatures as a formal stand-in for use cases. We learned to
distinguish between denitions and implementations, and
how to use the operations to understand the requirements
of your data model.
Summary
Function signatures has three parts: the name, the ar-
guments, and the return type. We use function signa-
tures to formally encode a use case.
There are four common operation patterns: construc-
tor, accessor, mutation, and combining functions. I’ve
listed them in the order of complexity, with combining
functions being the most complex. Dening those gives
us the most information.
A function denition implements a function in terms of
other domain operations. The functions we can’t dene
determine the requirements of the data encoding.
Total functions have valid return values for every val-
id argument. Total functions are more stable building
material.
There are three ways to make a partial function total:
augment the return, restrict the arguments, or change
the meaning. You can’t always do it, but changing the
meaning is the only one that relieves a burden.
There are three factors we need to prioritize: domain t,
elegance, and eciency. All three factors can help us
rene our designs.
Up next . . .
Weve learned a lot about operations. But operations are
rarely called by themselves. Instead, they’re called in com-
position with other operations. We want to ensure that
those compositions work correctly. That’s what we’ll see in
the composition lens.