← Contents · Runnable Specifications by Eric Normand · Work in progress · Comments
• 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 define.

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 you’re 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 functions
• constructor, accessor, mutation, and combining functions
• definition vs implementation

you’ll learn these terms in this chapter

95

• 96

all of these are mutation functions, which we’ll define in a few pages

Megabuzz touchscreen register

Here is the touchscreen the cashier sees as they take an order.

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 coffee order. And we’ve written them as function signatures. It turns out that function signatures are a great way to reason about operations. Let’s take a closer look at them on the next page.

Super

Raw

Soymilk

Espresso

Almond

Chocolate

Chapter 3

Hazelnut

Mega

Burnt

Galactic

Charcoal

+

+

+

+

+

2

1

0

0

0

-

-

-

-

-

function setSize(coffee, size)        //=> Coffee

function setRoast(coffee, roast)      //=> Coffee

• Function signature

The function signature is the part of the function without its body. It specifies the meaning separate from implementation. Here is the syntax we’ll use in the book:

97

Operation Lens

A function signature is the part of the function that tells us how to call it. In JavaScript, it is the first line of the function, along with the return type. It consists of three parts:

1. The name

The compiler doesn’t care about the function’s name, but it bridges the gap to human understanding. Choose names that use domain terms so the function matches the real-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 implicitly obtaining them in the body.

3. The return type

The return type explicitly describes the operation’s result. There should be no implicit outputs.

The function signature outlines all you need for later definition, serving as a crucial preparatory step. However, let’s hold off on defining the function until we’re sure we need it—there is no point in wasting effort prematurely.

The function signature is an abstract representation of the fully defined function. We use it to work and reason at the right level of abstraction, free from implementation details.

we will use the TypeScript name but with different syntaxt that fits on the page

the return type

the arguments

argument names imply their type (because JavaScript)

the function name

Function signatures are

• a concise and precise requirement
• the right level of abstraction for reasoning
• easy to implement later
• Constructor functions

Constructor functions are the simplest pattern of function. They are also known as factories. They take some parameters 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 often look like a waste. But they’re not a waste at this design stage, and they are important also during implementation.

They’re important during design because we don’t yet know how we will encode the data as a data structure. We need some way of representing the things we know (the parameters—size, roast, addIns) and what those turn into (the return type—Coffee). The constructor signature stands in for the data structure we will eventually use in the implementation.

Constructors are important during implementation because they can include validation of the arguments if we want to check them at runtime. We can also use the function as a first-class function where we pass it to other functions. We’re not deciding yet whether the implementation will have constructor functions—that’s for the implementation phase, and we’re still firmly in specification. 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 alternative.

98

Chapter 3

This way constructs a roast given a name.

• Accessor functions

Accessor functions are a sinple pattern of function. They are also known as queries. They take a value and answer a question about it, either directly plucked from the data or calculated 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 coffee?

Here is an accessor function that simply plucks the data right out of the data structure:

Q&A

Why are we defining a roast() function when the implementation is so trivial?

It’s true: We can implement roast() like this:

function roast(coffee) { //=> Roast
return coffee.roast;
}

Instead of writing roast(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 trying to separate specification from implementation. coffee.roast contains an implementation. roast(coffee), with only the function signature, does not have an implementation. It is pure specification.

When we separate out specification from implementation, we’re much more free to make better decisions, and hence design better software. We’re using the function signatures 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

99

Operation Lens

• Mutation function

Another common pattern is the mutation function. Mutation functions take an value, typically as the first argument, and return a modified copy of that value. We use them to encode operations that change state over time.

Remember, we’re using a functional style, so we can’t literally mutate the data representation of coffee. The best we can do is return a modified copy.

It’s 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 first. Mutation functions encode a discrete change from one valid state to another.

function setSize(coffee, size) //=> Coffee

takes a coffee, returns a modified 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 first argument
• return a modified copy of that value
• take parameters for the operation after the value 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

100

Chapter 3

• Combining function

The final function pattern we’ll talk about is the combining function. Combining functions are the most complex of the patterns we’ve seen so far. They take two (or more) arguments of the same type, combine them, and return the result.

This function takes two values of the same type (AddIns) and combines them into one value. We know a lot of operations like this already, such as

+, *, -, /

but also string concatenation and merging hash maps.

Combining functions tend to be more complex than mutation function because combining two values is inherently complex.

4 Function Patterns

1. Constructor functions
2. Accessor functions
3. Mutation functions
4. Combining functions

combining functions are the more complex of the four

101

Operation Lens

• 102

Operation complexity

We should start by defining the complex functions because in the long run it makes the work of design easier. The downside of complex functions is that they are harder to define. 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. Often, 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 unsuitable, and essential data may be missing. This can lead to a messy implementation, and you may have to scrap much of your work.

That’s why I recommend starting with combining functions and finding a straightforward way to define them. This will inform your choice of data encoding. The simple stuff will always be easy, so you can put those off until last. Let’s look at an example on the next page.

constructor functions

accessor functions

mutation functions

combining functions

simple

complex

Chapter 3

• Operation Lens

103

Worked example: Contact database

Let’s work through an example of how starting with accessors 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 modify the contacts’ 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 recommend) of starting with designing/implementing the simple functions, the constructor and the accessors. Because they can’t be defined in terms of other operations, we have to implement them, which means deciding on a data representation.

• 104

The mutation functions are also easy:

We’ll define 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

Chapter 3

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 combinations 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 productive work, but that’s misleading. Even though we have one function left—the combining function—that one function is going to cause us to throw away all the implementation code.

The combining function we have left is this one:

• Operation Lens

Now we are in the home stretch! We have one function left. syncContacts() takes two contacts that were saved on different devices, and returns a contact with the most recent version of all of the data.

For instance, let’s 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.

105

{
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 define the function that will decide the newest version of the contact given two current versions.

But we can’t define that function. How can we choose between the two phone numbers? We don’t have any information 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.

• How did we get here? By starting with the easiest function 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 information. We need to throw away all of our work because the encoding 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 don’t 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. Defining the complex function first gave us new information required for our design. Let’s look at how our design changes on the next page.

106

Chapter 3

Note

This is one possible definition. There’s a lot of possible improvement. You’re welcome to redesign it, but I won’t do it here. The point has been made.

• function getNameTime(contact) //=> Date
function getPhoneTime(contact) //=> Date

Defining 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-effect) and so it’s an implicit input. We want to encode our operations as calculations (pure functions).

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 define any of these functions in terms of the others. They need implementation details (the details of the data encoding). But syncContacts() was defined only in terms of these. Yet another reason to define it first.

We’ll see this contacts example again, and the interesting syncContacts() function, when we talk about the composition lens. This function is chock full of interesting properties.

107

Operation Lens

• Q&A

What’s the difference between a definition and an implementation?

That’s a tough question. People in our industry do not typically distinguish between them, but we should in this book.

A definition expresses the meaning of a domain concept in terms of the meanings of other domain concepts. A definition relates the meanings together. Here’s an example:

function coffeePrice(coffee) { //=> number
return sizePrice(size(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. Here’s an example implementation that assumes the encoding of the coffee and that add-ins are in a Map:

function coffeePrice(coffee) { //=> number
let price = sizePrice(coffee.size));
return price;
}

A definition is a kind of implementation, since it is code that gets the right answer. But a definition definitely cares about how it is implemented.

Why distinguish the two?

Our industry does not make this distinction, but it is essential for the skills in this book.

We want to separate thinking about the specification from thinking about the implementation. When we define a function in terms of other domain operations, we can design in the realm of domain meanings.

108

Chapter 3

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 define howManyAddIns() in terms of it:

)));
}

a === addIn ? q : 0
)));
}

Existing functions:

New functions:

function sum(numbers) //=> number
func /* AddIn, number -> T */
) //=> [T]

Some functions are defined in terms of others. Some are assumed 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 premises.

109

Operation Lens

Or we could leave howManyAddIns() as a premise.

• Total function

Total functions is an important idea from functional programming that will help us encode our models better.

A total function has a valid answer for every combination 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 undefined. 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, making 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
4. Do whatever (undefined 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 functions say what they do and do what they say.

We prefer total functions because they are a more stable 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 can’t 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;
}

110

Chapter 3

• 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 function 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 function signature. Let’s go over each of them for division.

First, recall the standard definition of divide():

function divide(dividend, divisor) //=> number | undefined

function divide(dividend, divisor) //=> number

function divide(dividend, divisor) { //=> number | undefined
if(divisor === 0)
return undefined;
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 undefined:

This method changes the return type, therefore changing the promise the signature is making. But notice what happened: Now the caller has to work with a more complicated return value. The return value could either be a number (great!) or undefined (which we’ll have to check for). Augmenting the return changes the burden from one of catching errors to handling different kinds of return values. The burden is still on the caller.

111

Operation Lens

We can implement it like this:

• 112

Another way to make a partial function total is to restrict the argument types. Let’s restrict divisor, the problematic argument.

One way to restrict it to make a new type of number that can never be zero. We’ll 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 define divide() like this:

This is probably close to what mathematicians mean when they say “division by zero is undefined.” Basically: Make sure you never divide by zero.

How do we get a NonZeroNumber? Well, divide() doesn’t need to care. But, since we’re curious, let’s implement everything, 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 tradeoff: Somewhere before divide() is called, something has to check if it’s zero (or check for null from the constructor). Restricting the arguments changes the burden 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 | nullfunction asNumber(nonZeroNumber) //=> number

constructor

the caller of the constructor has to check for null

Chapter 3

• Operation Lens

113

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 can’t really change the meaning of division, so it’s a bad example. But I can share another one.

In JavaScript, there is an operator called delete that removes a key/value from an object based on the key. Similar 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 functional-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 it’s not in the object, there’s no work to do, so we can return the object 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

The options for defining setSize()

We should list the options we have for defining our operations, then evaluate those options. Let’s start with setSize().

Although setSize() may appear straightforward, there are real choices to be made, and we should take the time to evaluate them. Remember: The quality of your design is proportional 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 coffee to super, that would throw an error.

Augment the return type

We could augment the return type with a value that indicates that nothing was changed. We could use any value that’s not a Coffee. Here, we’ll use false to indicate that nothing was changed.

Restrict the argument types

We could restrict arguments to values that are legal. Namely, 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

I’ll spill the beans: The last one is the best. But why? We’re trying to balance two of three factors.

Chapter 3

• Operation Lens

115

The three factors

Domain

Computer

Programmers

There are three factors any program needs to meet:

1. Domain - fit
2. Programmers - elegance
3. Computer - efficiency

Domain

The domain dictates the behavior of our software. We’ve already seen how an encoding can be evaluated against the domain (by way of the model) using fit. The domain is the most important force. Without the domain, our software doesn’t have a job. And without good fit, 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 handle incredibly complex code, like deeply nested conditionals, unintelligible algorithms, etc. They don’t mind.

But people do. We need to keep it simple, straightforward, and—quite frankly—beautiful. And we want it to use existing constructs.

We work all day in the code. We want to read it and modify it without unnecessary struggle. We have a wonderful aesthetic sense of elegance, which is the same principle that guides scientific theories toward truth. We should seek that truth in our code, too.

Computer

Eventually, our software will need to run on a computer. It needs to be efficient enough to do its job in a timely and cost-effective manner. We want to consider efficiency last, after we are sure we have the behavior we need. And, hopefully, we won’t need to optimize much at all.

low

high

from SICP

• List the operations

At the beginning of this chapter, we listed four operations that we extracted from the touchscreen interface the cashier uses. We used function signatures to encode the use cases of the domain.

However, there are more use cases that don’t 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 domain are the cashier, the barista (the coffee maker), and the marketers.

Cashier

The cashier says that they need a way to know the price of a coffee so that they can tell the customer how much to pay. We can turn that into a function signature:

116

Barista

When making the coffee, 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 they’re at the espresso machine, they see “0 soy shots.” As a function signature, this use case looks like:

Marketer

The marketers do research into the popularity of different add-ins. They want to know what percentage of coffees contain almond syrup, or how many coffees have espresso shots. They don’t care how many shots of almond syrup a coffee has, just yes or no, does it contain it:

There will be many more operations from all of the users of your software. 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

Chapter 3

• Operation Lens

117

Operations shed light on data encoding

In the Data Lens chapters, we had three ways we could encode 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 different 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 software and implement each one. Then we’ll compare them against our three criteria, fit, elegance, and efficiency.

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]])
...

• How does calculating the price affect data encoding?

Our cashiers need to know the price of a coffee to collect money from the customer.

function coffeePrice(coffee) //=> number

function coffeePrice(coffee) { //=> number
return sizePrice(size(coffee)) +
}

At MegaBuzz, the cost of a coffee is based on the size and the add-ins, as follows:

Notice that we’re definining 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

We will implement addInCollectionPrice() since it takes AddIns as an argument. Although we are still focused on specification instead of implementation, it can be valuable to probe the implementation to understand how the price calculation affects the add-ins collection encoding. We want to evaluate our possible encodings, so we need to implement it for each. We are looking for perfect fit and the most elegant solution.

when we define a domain operation, we do so in terms of other domain operations

Goal

Learn how coffeePrice() constrains add-ins encoding.

Plan

• find functions with AddInCollection arguments
• implement them for each possible encoding
• choose the most elegant definition

Let’s see the three implementations on the next page.

118

Chapter 3

• Implementing addInCollectionPrice() for 3 encodings

JS Object

.reduce((a, b) => a + b, 0)
);
}

Map

return (
.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

return (
.reduce((a, b) => a + b, 0)
);
}

Fit

All three implement the desired operation correctly and with perfect fit.

Elegance

The three implementations share a very similar structure, but the Array implementation is a clear win for elegance.

Efficiency

Of the three implementations, array does slightly better since it does not create the key/value entry pairs the others do.

We have a new operation to add to our list:

119

Operation Lens

• Now we can evaluate them against our three criteria.

Fit

All three implement the desired operation correctly and with perfect fit.

Elegance

JS Object and Map are more elegant. Array is less concise.

Efficiency

JS Object and Map are more efficient. The array implementation needs to do a linear scan over the array. The other two are done in constant time.

How does counting an add-in affect data encoding?

The barista needs to know how many add-ins to put at the station they are at:

Array

}

JS Object

}

Map

}

Let’s implement it for each possible encoding of AddInCollection.

120

Chapter 3

• How does checking an add-in affect data encoding?

The marketer needs to know if a coffee has a given add-in:

}

Well, here’s the surprising thing: Once we have howManyAddIns(), we can define hasAddIn() in terms of it:

121

Operation Lens

This definition is independent of how AddInCollection is encoded!

However, we should consider one thing: If we have one function defined 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?

• Using the spec to define the requirements of our encoding

One advantage of staying in the specification side (as opposed 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. Let’s see how to do that now for the AddInCollection.

Previously, we defined coffeePrice() like this:

It’s a true definition as opposed to an implementation because the meaning of the function is stated in terms of the meanings of other functions. But we implemented addInCollectionPrice() three different ways. Here is the map one:

Notice how the implementation details, especially the encoding of the AddInCollection, really complicated the function. But it doesn’t have to be this way. We were purposefully probing the implementation to understand how it would change based on the encoding.

But there’s another skill we can employ: That of incrementally refining a definition until we can’t define anything without knowing the implementation details. When we do that, we’ll get a precise specification of the requirements for our data encoding.

function coffeePrice(coffee) { //=> number
return sizePrice(size(coffee)) +
}

return (
.reduce((a, b) => a + b, 0)
);
}

122

Chapter 3

The first thing we need to do define 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 define it. We’ll have to evaluate all of the ways to find the best one. Here’s one way:

}

}

Existing functions:

New functions:

function sum(numbers) //=> number

sum() adds up numbers. eachAddIn() is a higher-order function that calls func() on each individual add-in, returning an array of the results. Now, this gives me an idea! We can define howManyAddIns() in terms of these as well:

123

Operation Lens

This will work, it has perfect fit, but it’s 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 it’s 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 definitions 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 arguments, and the return type. We use function signatures to formally encode a use case.
• There are four common operation patterns: constructor, accessor, mutation, and combining functions. I’ve listed them in the order of complexity, with combining functions being the most complex. Defining those gives us the most information.
• A function definition implements a function in terms of other domain operations. The functions we can’t define determine the requirements of the data encoding.
• Total functions have valid return values for every valid 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 fit, elegance, and efficiency. All three factors can help us refine our designs.

Up next . . .

We’ve learned a lot about operations. But operations are rarely called by themselves. Instead, they’re called in composition with other operations. We want to ensure that those compositions work correctly. That’s what we’ll see in the composition lens.