Functional Design Principles Patterns and Practices 1nbsped 0138176396 9780138176396
Functional Design Principles Patterns and Practices 1nbsped 0138176396 9780138176396
Robert C. Martin
v
Dedication
be. He said, “Rich!” I’m happy to report that he has done quite well. He
spent the better part of a decade working with me and then founded his
own software business, which he sold some years later. Then he spent a
year building an airplane in his garage. Now he’s running yet another
software business. Much of his success is due to Angelique, the beautiful,
hardworking, and deeply intelligent woman he married. They have raised
two spectacular young men.
Happy is the man who has his quiverful of children and grandchildren.
vi
Contents
Foreword xiii
Preface xv
Acknowledgments xxi
About the Author xxiii
Chapter 1 Immutability 3
What Is Functional Programming? 4
The Problem with Assignment 7
So Why Is It Called Functional? 10
No Change of State? 12
Immutability 15
vii
Contents
Chapter 4 Laziness 37
Lazy Accumulation 40
OK, but Why? 41
Coda 42
Chapter 5 Statefulness 43
When We MUST Mutate 47
Software Transactional Memory (STM) 48
Life Is Hard, Software Is Harder 51
viii
Contents
Clojure 88
Conclusion 93
ix
Contents
x
Contents
Afterword 337
Index 341
xi
This page intentionally left blank
Foreword
xiii
Foreword
Clojure’s critics will say that Clojure is unsuitable for any sufficiently large
codebase. As you’ll learn in the coming chapters, the design principles and
patterns apply to Clojure just as they do to Java, C#, or C++. The design
principles of SOLID will help you build better software with functional
programming. Design patterns have long since been scoffed at by
functional programmers, but Functional Design deconstructs such
criticism and shows exactly why developers need them, and how
developers can implement them on their own.
xiv
Preface
This is a book for programmers in the trenches who want to learn how
to use functional programming languages to get real things done. As such,
I will not spend any appreciable time on the more theoretical aspects of
functional programming such as Monads, Monoids, Functors, Categories,
and so on. Not that these ideas aren’t valid, valuable, or relevant; rather,
they do not often impact the day-to-day world of the programmer. This is
because they have already been “baked into the cake” of the common
languages, libraries, and frameworks. If you are interested in functional
theory, I recommend the writings of Mark Seemann.
xv
Preface
The two men independently proved that no such general solution exists
by demonstrating that there were integers that could never be calculated
by an integer formula smaller than the integer itself.
Another way to say this is that there are numbers that no computer
program can compute. And indeed, that was the approach that Alan
Turing took. In his famous 1936 paper,2 Turing invented a digital
computer, and then showed that there were numbers that could not be
computed—even given infinite time and space.3
Church, on the other hand, came to the same conclusion through his
invention of lambda calculus, a mathematical formalism for manipulating
functions. Using manipulations in the logic of his formalism, he was able
to prove that there were logical problems that could not be solved.
1. Diophantine equations.
2. A. M. Turing, “On Computable Numbers, with an Application to the Entscheidungsproblem”
(May 1936).
3. Given infinite time and space, a computer could calculate π or ϵ or any other irrational or tran-
scendental number for which a formula exists. What Turing and Church proved is that there were
numbers for which no such formula can exist. Such numbers are “uncomputable.”
xvi
Preface
Church and Turing later collaborated to show that Turing’s and Church’s
approaches were equivalent. That every program in a Turing machine can
be represented in lambda calculus, and vice versa.
O n C loju r e
I chose Clojure for this book because learning a new language and a new
paradigm is a doubly difficult task. Therefore, I sought to simplify that
task by choosing a language that is simple enough to not get in the way
of learning functional programming and functional design.
Having said all that, this book is not a Clojure tutorial.4 I will explain some
of the basics in the early chapters and use some explanatory footnotes
throughout the text, but I will also rely upon you, gentle reader, to do your
xvii
Preface
homework and look things up. There are several Web sites that will help.
One of my favorites is https://clojure.org/api/cheatsheet.
The test framework I used in this book is speclj.5 As the chapters progress,
you’ll see more and more of it. It is very similar to other popular testing
frameworks, so as the pages turn, you should not find it difficult to become
familiar with its various facilities.
O n “ Functional”
In this text, I will make use of the term functional. I will define it and
expound upon it. As the chapters roll by, I will also take some license
5. https://github.com/slagyr/speclj
6. Robert C. Martin, Clean Architecture (Pearson 2017), p. 57.
7. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of
Reusable Object-Oriented Software (Addison-Wesley, 1994).
xviii
Preface
Why take that license? Because this is a book about pragmatics, not
theory. I am more interested in extracting the benefits from the functional
style than in strict adherence to an ideal. For example, as we’ll see in the
first chapter, “functions” that take input from the user are not purely
functional. I will, however, make use of such “functions” as appropriate.
The source code for all the examples in all the chapters is in a single
GitHub repository named https://github.com/unclebob/FunctionalDesign.
xix
This page intentionally left blank
Acknowledgments
Thank you to the diligent and professional folks at Pearson for helping to
guide this book to completion: Julie Phifer, my ever-helpful, ever-supportive
publisher of long-standing; and her compatriots, Menka Mehta, Julie Nahil,
Audrey Doyle, Maureen Forys, Mark Taber, and a host of others. It has
always been a joy to work with you, and I look forward to many future
such endeavors.
Thank you to Jennifer Kohnke, who has produced the vast majority of the
gorgeous illustrations in my books over the last three decades. Back in 1995,
up against a production deadline, Jennifer, Jim Newkirk, and I pulled an
all-nighter to get the illustrations for my very first book formatted and
organized just the way I wanted.
xxi
Acknowledgments
Thanks to Stuart Halloway, who wrote the first book I read about Clojure.
It was more than a decade and a half ago that I started that adventure, and
I have never looked back. Stuart was kind enough to coach me through my
very first experiments with functional programming. Also to Stuart, an
apology for once, long ago, speaking out of turn.
Thank you to Janet Carr for her Foreword. I stumbled onto Janet’s work
while perusing Twitter one day and found that she had come to many
of the same conclusions regarding functional programming and Clojure
that I had.
And for writing the Afterword, thank you to Gina Martiny, my lovely
daughter and an accomplished chemical and software engineer. More
about her in my dedication.
xxii
A bout the Author
xxiii
This page intentionally left blank
I
Functional Basics
1
This page intentionally left blank
1
I mmutabilit y
3
Chapter 1 Immutability
While these assertions might be true, they are not particularly helpful. I
think a better answer is: Programming without assignment statements.
Perhaps you don’t think that definition is much better. Perhaps it even
frightens you. After all, what do assignment statements have to do with
functions; and how can you possibly program without them?
This program is the core loop of virtually every program ever written. It
quite literally says: “Do something until you are done.” What’s more, this
program has no visible assignment statements. Is it functional? And if so,
does that mean every program ever written is functional?
Let’s actually make this function do something. Let’s have it compute the
sum of the squares of the first ten integers [1..10]:
int n=1;
int sum=0;
4
What Is Functional Programming?
int done() {
return n>10;
}
void doSomething() {
sum+=n*n;
++n;
}
void sumFirstTenSquares() {
while(!done())
doSomething();
}
int sumFirstTenSquares() {
int sum=0;
int i=1;
loop:
if (i>10)
return sum;
sum+=i*i;
i++;
goto loop;
}
This is better; the two globals have become local variables. But it’s still not
functional. Perhaps you are worried about that goto. It is there for a good
reason. Bear with me as you consider this small modification that uses a
worker function to convert the local variables into function arguments:
5
Chapter 1 Immutability
sum+=i*i;
i++;
goto loop;
}
int sumFirstTenSquares() {
return sumFirstTenSquaresHelper(0, 1);
}
This program is still not functional; but it’s an important milestone that
we’ll refer to in a moment. But now, with one last change, something
magical happens:
int sumFirstTenSquares() {
return sumFirstTenSquaresHelper(0, 1);
}
All the assignment statements are gone, and this program is functional. It’s
also recursive. That’s no accident. If you want to get rid of assignment
statements, you have to use recursion. Recursion allows you to replace the
assignment of local variables with the initialization of function arguments.
It also burns up a lot of space on the stack. However, there is a little trick
we can use to fix that problem.
Notice that the last call to sumFirstTenSquaresHelper is also the last use of
sum and i in that function. Holding those two variables on the stack after
initializing the two arguments of the recursive call is pointless; they’ll never
be used. What if, instead of creating a new stack frame for the recursive
call, we simply reused the current stack frame by jumping back to the top
of the function with a goto, as we did in the milestone program?
6
The Problem with Assignment
This cute little trick is called tail call optimization (TCO) and all
functional languages make use of it.1
Notice TCO effectively turns that last program into the milestone
program. The last three lines of sumFirstTenSquaresHelper in the
milestone program are, in effect, the recursive function call. Does that
mean the milestone program is functional too? No, it just behaves
identically. At the source code level, that program is not functional
because it has assignment statements. But if we take one step back and
ignore the fact that the local variables changed as opposed to being
reinstantiated in a new stack frame, then the program behaves as a
functional program.
int x=0;
x=1;
1. In one way or another. The Java virtual machine (JVM) complicates TCO a bit. C, of course, does
not do TCO and so all my recursive examples in C will grow the stack.
7
Chapter 1 Immutability
In the first case, the variable x comes into existence with the value 0;
prior to the initialization, there was no variable x. In the second case,
the value of x is changed to 1 . This may not seem significant, but the
implications are profound.
.
//Block A
.
x=1;
.
//Block B
.
The state of the system during the execution of Block A is different from
the state of the system in Block B. This means that Block A must execute
before Block B . If the position of the two blocks were swapped, the
system would likely not execute correctly.
How many times have you forgotten to close a file, or release a block of
memory, or close a graphics context, or release a semaphore? How many
8
The Problem with Assignment
times have you debugged a pernicious problem only to find that you can
fix it by swapping the position of two function calls?
And that doesn’t take into account multiple threads. When two or more
threads are competing for the processor, keeping the temporal couplings
in the correct order becomes a much more significant challenge. Those
threads may get the order correct 99.99 percent of the time; but every
once in a great while they may execute in the wrong order and cause all
manner of mayhem. We call those situations race conditions.
But perhaps it’s time for a simple example. Here’s our nonfunctional
algorithm again; this time without the goto:
9
Chapter 1 Immutability
4: i++;
5: }
6: return sum;
7: }
Now let’s say you’d like to log the progress of the algorithm with a
statement like this:
Where would you put that line? There are three possibilities. If you add the
log statement after line 2 or 4, then the logged data will be correct, and
the difference will simply be whether you are logging before or after the
computation. If you insert the log statement after line 3, then the logged
data will be incorrect. That is a temporal coupling—an ordering problem.
There is only one place we can put our log statement, and it will log
correct data.
S o Wh y I s It Ca lle d Functional?
A function is a mathematical object that maps inputs to outputs. Given
y = f(x), there is a value of y for every value of x. Nothing else matters
to f. If you give x to f, you will get y every single time. The state of the
system in which f executes is irrelevant to f.
10
So Why Is It Called Functional?
int sumFirstTenSquares() {
return sumFirstTenSquaresHelper(0, 1);
}
int sumFirstTenSquares() {
return (1>10) ? 0 : sumFirstTenSquaresHelper(0+1*1, 1+1);
}
int sumFirstTenSquares() {
return
(1>10) ? 0 :
(2>10) ? 0+1*1
: sumFirstTenSquaresHelper((0+1*1)+2*2,
(1+1)+1);
}
I think you can see where this is going. Each call to sumFirstTenSquares
Helper simply gets replaced with its implementation with the arguments
properly replaced.
11
Chapter 1 Immutability
Notice that you cannot do this simple replacement with the nonfunctional
version of the program. Oh, you can unwind the loop if you like; but
that’s not the same as simply replacing each function call with its
implementation.
N o C h a nge of State ?
If there are no variables in functional programs, then functional
programs cannot change state. How can we expect a program to
be useful if it cannot change state?
The answer is that functional programs compute a new state from an old
state, without changing the old state. If this sounds confusing, then the
following example should clear it up:
State system(State s) {
return isFinal(s) ? s : system(s);
}
You can start the system in some initial state, and it will successively
move the system from state to state until the final state is reached. The
system does not change a state variable. Instead, at each iteration, a new
state is created from the old state.
If we turn TCO off and allow the stack to grow with each recursive call,
then the stack will contain all the previous states, unchanged. Moreover,
the system functions as a true function in the mathematical sense. If you
call system with state1 , it will return state2 every single time.
12
No Change of State?
Now, the computed next state of the system is a function of the current
state and an incoming event. And voila! We have created a very traditional
finite state machine that can react to events in real time.
Notice the quotes I put around the word functional above. That is
because getEvent is not referentially transparent. Every time you call it
you will get a different result. Thus, you cannot replace the call with its
return value. Does this mean that our program is not actually functional?
Strictly speaking, any program that takes input in this manner cannot be
purely functional. But this is not a book about purely functional programs.
This is a book about functional programming. The style of the program
above is “functional,” even if the input is not pure; and it is that style we
are interested in here.
#include <stdio.h>
void lock() {
printf("Locking.\n");
}
13
Chapter 1 Immutability
void unlock() {
printf("Unlocking.\n");
}
void thankyou() {
printf("Thanking.\n");
}
void alarm() {
printf("Alarming.\n");
}
Event getEvent() {
while (1) {
int c = getchar();
switch (c) {
case 'c': return coin;
case 'p': return pass;
case 'q': return quit;
}
}
}
case pass:
alarm();
return locked;
case quit:
return done;
}
14
Immutability
case unlocked:
switch (e) {
case coin:
thankyou();
return unlocked;
case pass:
lock();
return locked;
case quit:
return done;
}
case done:
return done;
}
}
State turnstileSystem(State s) {
return (s==done)? 0
: turnstileSystem(
turnstileFSM(s, getEvent()));
}
Keep in mind that C does not use TCO, and so the stack will grow until it
is exhausted—though that may require quite a few operations in this case.
I m m uta bilit y
What all this means is that functional programs contain no variables.
Nothing in a functional program changes state. State changes are passed
from one invocation of a recursive function to the next, without altering
any of the previous states. If those previous states aren’t needed, TCO can
15
Chapter 1 Immutability
optimize them away; but in spirit they all still exist, unchanged,
somewhere in a past stack frame.
16
2
Persistent Data
17
Chapter 2 Persistent Data
So far this has seemed relatively simple. Programs written in the “functional”
style are simply programs that have no variables. Rather than reassign values
to variables, we use recursion to initialize new function arguments with new
values. Simple.
But data elements are seldom as simple as we have so far imagined them
to be. So let’s take a look at a slightly more complicated problem, The
Sieve of Eratosthenes:
package sieve;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
18
On Cheating
primes.add(i);
return primes;
}
}
This cute little Java program computes the prime numbers up to a limit.
Notice all the assignment statements. There are variables everywhere, so
this program must not be functional.
But then again, look at the static function at the top. Sieve.primesUpTo is
a true mathematical function. Every time you call it with n, it will return
the prime numbers up to n. So we can cheat and say that despite the fact
that the underlying algorithm uses variables, the result of that algorithm
is functional.
O n C he ating
Our computers are, in some sense, finite Turing machines; they are not
based upon lambda calculus. The Church–Turing thesis tells us that Turing
machines and lambda calculus are equivalent forms; but that doesn’t mean
you can easily translate from one to the other. A functional program is a
program that looks like lambda calculus but is implemented in a finite
Turing machine. And that implementation requires that we cheat.
The first cheat we saw was TCO. We waved it away with an argument
about pragmatics. After all, since we were never going to need all those
historical stack frames, why should we keep them? But that’s still a cheat.
Under the hood, our implementation was changing the values of existing
variables. From the Turing machine’s point of view, all our supposed
constants were actually variables.
We could continue to push that cheat upward. This lovely little Sieve
algorithm runs entirely in the constructor, so it’s all initialization! And as
we learned, initialization is not assignment. So the fact that this program
has variables under the hood is no different from TCO. In the end, the
result is still functional.
19
Chapter 2 Persistent Data
This is fun! We can keep pushing that cheat upward. We can push it
up until it is outside our finite Turing machine of a computer. And then
we could say to ourselves: “Every program that runs in this computer
is functional because it will always produce the same outputs when
given the same inputs. Never mind that the inputs and outputs include
every single bit in the computer’s memory. Never mind that. Yeah.
That’s the ticket.”
Of course, if we take that view, then there’s not much point in studying
functional programming, is there? So let’s back down from that highest-
level cheat and keep pushing the cheats back down until we simply
cannot practically escape them.
M aking Copie s
So, what about that Sieve algorithm: Can we push the cheating down
lower than that? Can we write that algorithm so it does not use any
assignment statements?
The problem, of course, is all those for loops. We need to turn those into
recursive functions in order to get rid of the assignment statements. We also
need to do something about the two arrays. We can’t be changing elements
in existing arrays, can we? That would make those arrays variables. So
we’ll have to make copies of them whenever we need to change an element:
package sieve;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
20
Making Copies
21
Chapter 2 Persistent Data
That’s not very pretty, is it? It is, however, pretty functional. You might
complain about the assignments in makeSieve, and I agree that’s a bit of a
cheat, but it looks close enough to an initialization to satisfy me.
So, yes, all the significant assignment operations have been eliminated. All
the named entities are constants, and the stack (if not deleted by TCO)
contains the history of each invocation of each recursive function.
But at what cost? Every time either of the two arrays is modified, a new
array is created in order to prevent the previous one from being changed.
The amount of memory used by this algorithm could be enormous.
Imagine finding all the primes up to 100,000. How many sieve arrays
would be created? How many primes arrays?
And what about execution time? Copying all those arrays over and over
again must eat up a terrifying number of cycles.
Is that, then, the cost of functional programming? Must we live with such
a huge extravagance of memory and time?
22
Structural Sharing
Structu r al S h a ring
Fortunately, no. It turns out that there are data structures that behave
very much like arrays but that also efficiently maintain the history of
their past states. These data structures are n-ary trees. The bigger the n,
the more efficient they are. But for the sake of simplicity, I will choose an
n of 2—binary trees—for the following examples.
1 2 3 4 5 6 7 8
If you look at the leaves and ignore the branches, you will see that the
leaves form an array. The branches simply provide a way to traverse to
each leaf in some ordered way. That order is the index of the array!
To get to the element at index 0 of the array, simply take the leftmost
branch of each node. To get to the element at index 1, go left at each
node but right at the last node.
I won’t belabor this point. I’m sure you all understand binary trees.
Now, let’s say we want to append a 42 on the end of this array while
preserving the existence of the previous array. The binary tree that
achieves this is shown in Figure 2.2.
23
Chapter 2 Persistent Data
[1..8,42]
[1..8]
1 2 3 4 5 6 7 8 42
Figure 2.2. A binary tree that represents [1..8, 42] but also preserves the original [1..8] array
Now the tree has two roots. The root at the top left still represents the
array from 1..8. The root at the top right represents the new array with a
42 appended after the 8.
Stop now and think carefully about this. It should be clear that representing
linear arrays as trees, in the manner shown, will allow us to represent
additions, insertions, and deletions while preserving all previous
arrangements, without massive copying of the array.
Oh, there is some copying going on. We may have to copy a leaf node,
or some of the branch nodes, depending on what operation we are
performing. But the amount of memory and the number of cycles are
drastically less than simply maintaining copies of all the past versions
of the array.
In the end, every past version of the array will be represented by a new
root node connected to a small number of additional branch nodes,
allowing the majority of the elements of the array to be shared among all
the versions.
Now consider what happens if we use 32-ary trees instead of binary trees.
For arrays of a million elements, the tree depth is on the order of four or
five branches. Copying five nodes of 32 elements each is a lot faster and
24
Structural Sharing
requires a lot less memory than copying a million elements. Indeed, the
cost, while not zero, is so small as to be inconsequential for most
applications.
But what about higher-level data structures like hash maps, sets, stacks,
and queues? How do we make all of them as persistent as our linear
indexed array? Of course, all those data structures can be implemented
using indexed arrays. Indeed, since the memory of the computer is nothing
more than one big indexed linear array, every data structure that you can
represent within a computer can also be represented in a persistent array.
And so the problem we confronted at the start of this chapter, the problem
of copying, can be set aside. The cost of functional programming, in
memory and cycles, need not dissuade us from further study and pursuit
of the benefits of functional programming.
And with that problem solved, all future examples will be written in
Clojure, a language that intrinsically supports persistent data structures.
1. Not to be confused with the overloaded term used to describe data in offline storage.
25
This page intentionally left blank
3
R ecursion and
Iter ation
27
Chapter 3 Recursion and Iteration
Ite r ation
TCO is the remedy for the infinite stack depth implied by infinite recursive
loops. However, TCO is only applicable if the recursive call is the very last
thing to be executed within the function. Such functions are often called
tail call functions.
(fibs 15)
28
Iteration
That’s not much of an exaggeration. The syntax of Lisp is really that simple.
The syntax of Clojure is just a bit more complicated. So let’s take the
above program apart, one statement at a time.
First there’s defn, which looks like it is being called as a function. Let’s go
with that for now. The truth is mostly compatible with that view. So the
defn “function” defines a new function from its arguments. The functions
being defined are named fibs-work and fibs. The square brackets after
the function name enclose the names of the arguments of the function.1
So the fibs function takes a single argument named n, while the fibs-
work function takes three arguments named n, i, and fs.
Following the argument list is the body of the function. So the body of the
fibs function is a call to the cond function. Think of cond like a switch
statement that returns a value. The fibs function returns the value returned
by cond.
The arguments to cond are a set of pairs. The first element in each pair
is a predicate, and the second is the value that cond will return if that
predicate is true. The cond function walks down the list of pairs until it
sees a true predicate, and then it returns the corresponding value.
1. Actually, the square brackets are Clojure syntax for a “vector” (an array). In this case, that vector
contains the symbols that represent the arguments.
29
Chapter 3 Recursion and Iteration
The predicates are just function calls. The (< n 1) predicate simply calls
the < function with n and 1. It returns true if n is less than 1. The (= n 1)
predicate calls the = function, which returns true if its arguments are
equal. The :else predicate is considered true.
The value returned by cond for the (< n 1) predicate is [], an empty
vector. If (= n 1), then cond returns a vector containing 1. Otherwise,
cond returns the value produced by the fibs-work function.
Got it? Make sure you do. Go back over it until you do.
The ))) at the end of the fibs function are just the closing parentheses of
the defn, cond, and fibs-work function calls. I could have written fibs
like this:
Perhaps that makes you feel better. Perhaps that relieves the eyestrain
headache you felt coming on. And indeed, many new Lisp programmers
use this technique to reduce their parentheses anxiety. That’s certainly
what I did a decade and a half ago when I first started learning Clojure.
30
Iteration
Anyway, that brings us to the heart of the matter, the fibs-work function.
If you have gotten comfortable with the fibs function, you have probably
already worked out most of the details of the fibs-work function. But
let’s go through it step by step just to be sure.
So, if (= i n), then we return fs. Otherwise… Well, let’s walk through
that one carefully.
It is the conj function that does the appending. It takes two arguments: a
vector and the value to append to that vector. Vectors are a kind of list.
We’ll talk about them later.
The apply function takes two arguments: a function and a list. It calls the
function with the list as its arguments. So, (apply + [3 4]) is equivalent
to (+ 3 4).
31
Chapter 3 Recursion and Iteration
OK, so now you should have a good working grasp of Clojure. There’s
more to the language that we’ll encounter as we go along. But for now,
let’s get back to the topic of iteration and recursion.
Ite r ation
Notice the recursive call to fibs-work is a tail call. The very last thing
done by the fibs-work function is to call itself. Therefore, the language can
employ TCO to eliminate previous stack frames and turn the recursive call
into a goto, effectively converting the recursion to pure iteration.
So, then, functions that employ tail calls are, for all intents and purposes,
iterative.
The recur function can only be called from a tail position, and it
effectively reinvokes the enclosing function without growing the stack.
R ecur sion
There is a much more natural and elegant way to write the Fibonacci
algorithm using true recursion:
32
Recursion
(<= n 2) 1
:else (+ (fib (dec n)) (fib (- n 2)))))
The fib function should be self-explanatory by now. After all, fib(n) is just
fib(n−1) + fib(n−2). Notice, however, the calls to fib are not on the tail of
the function. The last thing executed by the :else clause is the + function.
This means we cannot use the recur function and that TCO is not
possible. This also means that the stack will grow as the algorithm proceeds.
The range function takes two arguments, a and b, and returns a list of all
the integers from a to b−1. The map function takes two arguments, f and
l. The f argument must be a function and the l argument must be a list. It
calls f with each member of l and returns a list containing the results.
fib 20 = 6765
"Elapsed time: 1.459277 msecs"
fib 25 = 75025
"Elapsed time: 11.735279 msecs"
fib 30 = 832040
"Elapsed time: 106.490355 msecs"
fib 34 = 5702887
"Elapsed time: 735.689834 msecs"
I didn’t bother to analyze the algorithm. But a quick curve fit suggests
that the algorithm is O(n3). So, as elegant as the implementation appears,
it will never do.
(defn ifib
([n a b]
33
Chapter 3 Recursion and Iteration
(if (= 0 n)
b
(recur (dec n) b (+ a b))))
([n]
(cond
(< n 1) nil
(<= n 2) 1
:else (ifib (- n 2) 1 1)))
)
The ifib function has two overloads: [n a b] and [n]. Since it is iterative,
it does not grow the stack, and it is also much faster than the previous
recursive version. Indeed, I believe most of that time was spent in printing
rather than true computation.
ifib 20 = 6765
"Elapsed time: 0.185508 msecs"
ifib 25 = 75025
"Elapsed time: 0.177111 msecs"
ifib 30 = 832040
"Elapsed time: 0.14596 msecs"
ifib 34 = 5702887
"Elapsed time: 0.148221 msecs"
(declare fib)
34
Recursion
(<= n 2) 1
:else (+ (fib (dec n)) (fib (- n 2)))))
This version of the algorithm is just as fast as the iterative version because
we have short-circuited the vast majority of the recursion without sacrificing
the elegance of the algorithm. We pay for that with a little extra memory,
but that seems a small price to pay.
fib 20 = 6765
"Elapsed time: 0.168678 msecs"
fib 25 = 75025
"Elapsed time: 0.16232 msecs"
fib 30 = 832040
"Elapsed time: 0.151619 msecs"
fib 34 = 5702887
"Elapsed time: 0.15134 msecs"
What we have learned here is that iteration and recursion are very
different approaches. Iterative functions must use tail calls to drive the
iteration and should use TCO to prevent the growth of the stack.
Recursive functions do not use tail calls and therefore will grow the
stack. Truly recursive functions can be quite elegant, and memoization
can be used to prevent that elegance from significantly affecting
performance.
35
Chapter 3 Recursion and Iteration
Although Clojure was used as the language in this chapter, the concepts
are the same in virtually every other functional language, and could even
be implemented in nonfunctional languages, though with a substantial
loss of elegance. ;-)
36
4
L a ziness
37
Chapter 4 Laziness
(declare fib)
(defn lazy-fibs []
(map fib (rest (range)))
)
The lazy-fibs function may look a little strange to you. Let’s walk
through it. You already understand the map function. The rest function
takes a list and returns that list without the first element. And that brings
us to the range function.
What is a lazy list? A lazy list is an object that knows how to compute its
next value. In Java, C++, and C#, we called such objects iterators. A lazy
list is an iterator masquerading as a list.
Clojure is friends with lazy lists. Most of the library functions return lazy
lists if possible. So, in the above program, rest and map both return a
lazy list. And that means lazy-fibs also returns a lazy list.
38
Laziness
(take 10 (lazy-fibs))
returns: (1 1 2 3 5 8 13 21 34 55)
The take function takes two arguments: a number n and a list. It returns
a list that contains the first n elements of the argument list. Actually,
that’s not quite right, but I’ll get to that in a minute.
So, now let’s walk through lazy-fibs again. The range function returns
a lazy list of integers starting at zero. The rest function takes that list,
drops the first element, and then returns a lazy list of the remaining
integers, which in this instance, are the integers starting at one. The map
function applies each of those integers to the fib function returning a
lazy list of the Fibonacci numbers starting at (fib 1).
You can have as many Fibonacci numbers as you like, so long as there
are no overflows or other machine limitations. So, for example:
The nth function takes a list and an integer n and returns the nth element
of the list. So this returns the 50th Fibonacci number.
The def function (it’s not really a function, but pretend that it is) creates a
new symbol and associates it with a value. So the symbol list-of-fibs
refers to a lazy list of Fibonacci numbers, as you can see from the following:
(take 5 list-of-fibs)
returns: (1 1 2 3 5)
39
Chapter 4 Laziness
for Fibonacci numbers. The calculations only take place, and the memory is
only allocated, as the elements of the list are accessed. Remember, behind
the scenes, the lazy lists are really just iterators that know how to calculate
their next element. Once that calculation takes place, the memory is
allocated and the value is placed into a real list.1
L a z y Accu m ul ation
It should be clear that if you continue to pass lazy lists through functions
like map, rest, and take (yes, take actually returns a lazy list), you will
accumulate a long chain of iterators behind the scenes. Each of those
iterators must hold on to the function that calculates its next value. It
must also hold on to all the data required for that calculation.
This works fine until you run out of the memory allocated for holding all
those deferred iterators. So, from time to time, it might be a good idea to
convert your lazy lists into real lists. In Clojure, we do that with the
doall function:
1. That’s a convenient way to think of it for now. Actually, as we’ll see shortly, the memory is only
allocated and the list only grows, if the program needs to hold those values.
40
OK, but Why?
O K , but Wh y?
Good question! Laziness is not free. It requires memory and cycles to
defer calculations. Then there’s the problem of accumulation that can lead
to memory exhaustion.
Because laziness decouples what you need to do from how much you
need to do. You can write a program that creates a lazy sequence without
knowing how big a sequence your users are going to want. Your users
can determine how much of your sequence they need.
returns 22559151616193633087251269503607207204601132491375819058863
➥8866418474627738686883405015987052796968498626N
Or, consider this example. I could create a list of 51 integers like this:
(range 51)
41
Chapter 4 Laziness
Or like this:
(take 51 (range))
Notice in the first example, the 51 is far more coupled than in the second.
In the first, I have to get that 51 into the range function somehow. I might
be able to pass it as an argument, but that’s a pretty strong coupling. In the
second example, the range function doesn’t care at all. That 51 could be
way out in some other part of the code, far removed from the call to range.
By the way, you might be interested to know that in the lazy-fibs example
above, (fib 1) through (fib 499) have likely been garbage-collected. Since
I’m not holding on to the list itself, the runtime system is free to dispose of
the previously calculated elements. Thus, it would be possible to create and
traverse a lazy list with trillions of elements and yet never hold more than
one2 of them in memory at a time.
Coda
There is much more to learn about laziness. My purpose here has been to
make you aware of it because it is so common in functional languages.
We will be seeing much more of it in the pages to come, but it will almost
always be in the background.
2. Or at least some n, where n is small and is the “chunk” size of the lazy engine.
42
5
Statefulness
43
Chapter 5 Statefulness
In the end, every program ever written is just a form of y = f(x), where x
is all the input you give to the program and y is all the output it delivers
in response.
This definition is sufficient for all batch jobs. For example, in a payroll
system, the input x is all the employee records and timecards and the
output y is all the paychecks and reports.
But perhaps this batch definition is too simplistic. After all, in interactive
applications, the input you give to the program is often based on the
output it just gave you. So perhaps we should think of interactive software
systems as:
void p(Input x) {
while (x != DONE)
x = (getInput(f(x))
}
In other words, our program is a loop that computes y = f(x) and then
hands y to some source of input that is passed back into f until f finally
returns DONE.
In some very real sense, the state of this program during each iteration is
x. If you were debugging some malfunction, you would want to know the
value of x and would likely call x the state of the system.
And indeed, in the program above, there is a variable named x that holds
the state of the system and is updated upon each iteration.
void p(Input x) {
if (x!=DONE)
p(getInput(f(x)));
}
44
Statefulness
Now this program has no variable that is updated to hold the state of the
system. Instead, that state is passed as an argument from one invocation
of p to the next.
A few years ago I wrote a functional program in Clojure that looked very
much like this. It was a version of the old computer game Spacewar!. You
can see (and play) this program at https://github.com/unclebob/spacewar.
The game is visual and interactive, and it is written in the “functional” style.
In other words, the spacewar program is a loop that exits if the :done?1
attribute of the world is true, and otherwise presents the world to the user
and gets input that it uses to update the world.
1. Keywords in Clojure are prefixed with colons. So :done? is a keyword, which is just a constant
that can be used as an identifier. Often, they are used as keys into hash maps. When used as a func-
tion, a keyword behaves like an accessor into a hash map. Thus, (:done? world) simply returns
the :done? element of the world hash map.
45
Chapter 5 Statefulness
(game-over ms)
(ship/update-ship ms)
(shots/update-shots ms)
(explosions/update-explosions ms)
(clouds/update-clouds ms)
(klingons/update-klingons ms)
(bases/update-bases ms)
(romulans/update-romulans ms)
(view-frame/update-messages ms)
(add-messages)
))
The threading macro (->>) simply passes the argument world into game-won,
the output of which gets passed to game-over, the output of which gets
passed to ship/update-ship, and so on. Each of those functions returns an
updated version of the world.
I’m showing this to you to give you a glimpse of the complexity being
managed by this program. Keep in mind that the world is not a mutable
variable. Each of those threaded functions into which the world is being
passed is returning a new version of the world and passing it to the next.
It is not being held in a variable and being mutated.
46
When We MUST Mutate
::weapon-spread-setting
::heading-setting
::antimatter ::core-temp
::dilithium ::shields
::kinetics ::torpedos
::life-support-damage ::hull-damage
::sensor-damage ::impulse-damage
::warp-damage ::weapons-damage
::strat-scale
::destroyed
::corbomite-device-installed]))
What you are looking at is a small portion of the type specification of the
Enterprise, the player’s ship. Clojure provides a mechanism called clojure
.spec that give us the ability to very specifically design our data structures
with even more precision and control than most statically typed languages.
The bottom line here is that there is no level of complexity that demands that
we abandon immutability and deviate from the functional style. On the other
hand, there are other factors that do, from time to time, make that demand.
Whe n We M U ST M utate
The spacewar program uses a graphical user interface (GUI) framework
called Quil.2 This framework allows the programs that use it to be written
in a “functional” style. It may not actually be functional in its internals, but
from the outside looking in, there need not be any visible mutable state.
2. See www.quil.info. Quil uses Processing behind the scenes. Processing is a Java framework that is
certainly not functional. Quil pretends to be functional by hiding the mutable variables, or at least
by not forcing you to mutate those variables.
47
Chapter 5 Statefulness
Swing is not the only framework that forces you into the mutable world.
There are many others. So, even if you are determined to use the “functional”
style, you must be able to deal with the fact that a large panoply of existing
software frameworks will force you out of that style.
Worse, many such frameworks also force you into the multithreaded
world. Swing, for example, runs in its own special thread. Programmers
should not use that thread for regular processing but must specifically
enter that thread when mutating Swing data structures.
This puts the users of such frameworks into the double jeopardy of
mutating state from within multiple threads. The dreaded result of that,
of course, is race conditions and concurrent update anomalies.
3. https://github.com/unclebob/more-speech
48
Software Transactional Memory (STM)
The problem is that f takes time to do its work, and there is a chance that
some other thread will interrupt f and apply its own operation g on o: og
= g(o). When f finally completes, what is the state of o? Is it of? Or is it
og? Or have both mutations been applied, giving us of g?
The typical concurrent update problem would most often yield of , causing
the operation of g to be lost. Programmers often resolve this kind of
problem by locking o so that g cannot interrupt f, and vice versa. The lock
forces the interrupting thread to wait until o is unlocked. The problem,
however, is that this can lead to the dreaded deadly embrace.4
Imagine that we have two objects o and p and two functions f(o, p) and
g(p, o). These functions lock their arguments before operating on them.
Suppose f and g are executing in different threads and g interrupts f just
after f locks o. Now g locks p but cannot lock o because o is locked by f,
so g waits. Now f wakes up and tries to lock p but cannot because p is
locked by g—and nothing can proceed. The functions f and g are in a
deadly embrace.
STM solves this problem by not locking, and instead using a commit/
rollback technique. Let’s call this technique swap. We can enact it
with swap(o, f), which will hold the current value of o in oh, compute
49
Chapter 5 Statefulness
There are several ways to use STM in Clojure, but the simplest is the
atom. An atom is an atomic value that can be altered using the swap!
function. Here’s an example:
(defn -main []
(let [ta (future (increment 10 "a"))
tx (future (increment 10 "x"))
_ @ta
_ @tx]
(println "\nCounter is: " @counter)))
The first line creates the atom named counter. The -main program starts
two threads, using future, both of which call the increment function. The
@ta and @tx expressions wait for the respective threads to complete.
50
Life Is Hard, Software Is Harder
The add-one function adds one to its argument, but that print function
can allow another thread to jump in; and that’s exactly what happens.
Here’s an example of the output:
a(0)a(1)a(2)a(3)a(4)xa(5)x(5)(6)(6)x(7)(7)a(8)(8)
x(9)(9)a(10)(10)x(11)a(11)(12)(12)a(13)x(13)(14)(14)
x(15)(15)(16)x(17)x(18)x(19)
Counter is: 20
At first, thread a runs without interruption for a while. But at the fifth
increment, the x thread jumps in, and the two fight each other. Notice the
repeated values as the swap! detects the collisions and repeats. Finally,
thread a finishes and thread x experiences no further interruptions. The
end count of 20 is correct.
51
This page intentionally left blank
Compar ative
Analysis
II
What follows is a comparative analysis of a series of exercises written in
traditional object-oriented (OO) style and in “functional” style. The first
two exercises may appear familiar to you; the OO portions come from
examples that I published in Clean Craftsmanship.1
Both versions of each of the examples were created using the discipline
of test-driven development (TDD). The tests are shown with the code in
an incremental fashion. You’ll see how the first test was passed, then the
second, then the third, and so on.
The point of this part of the book is to explore and examine the differences
between OO implementations and functional implementations.
The exercises increase in complexity from one to the next. Prime Factors
is pretty simple. Bowling Game is a bit more complicated and Gossiping
Bus Drivers is more complicated still. The last exercise, Payroll, is the
most complex of the examples. I explored it in great detail in Section 3 of
Agile Software Development: Principles, Patterns, and Practices.2 So to
save space I’ve only included the functional version.
53
Part II Comparative Analysis
54
6
Prime Factors
55
Chapter 6 Prime Factors
J ava Ve r sion
We begin with a simple test:
assertThat(factorsOf(2), contains(2));
56
Java Version
factors.add(2);
return factors;
}
Next comes 3,
assertThat(factorsOf(3), contains(3));
which we make pass by being a bit clever and replacing the 2 with n:
Next comes 4, which is the first time our list will have more than one
factor in it:
57
Chapter 6 Prime Factors
assertThat(factorsOf(5), contains(5));
assertThat(factorsOf(6), contains(2,3));
assertThat(factorsOf(7), contains(7));
The 8 case is the first time we’ve seen more than two elements in the list
of factors:
The next test, 9, must also fail because nothing in our solution factors out 3:
58
Java Version
factors.add(2);
n /= 2;
}
while (n % 3 == 0) {
factors.add(3);
n /= 3;
}
}
if (n>1)
factors.add(n);
return factors;
}
59
Chapter 6 Prime Factors
return factors;
}
And that algorithm is sufficient to compute the prime factors of any4 integer.
C loju r e Ve r sion
OK, so what does this look like in Clojure?
And we make that pass as one might expect, by returning an empty list:
And the solution to the third test employs the same clever replacement of
2 by n:
60
Clojure Version
But with the test for 4, the Clojure and Java solutions begin to diverge:
At this point in the Java program, there was no iteration. The two if(n>1)
segments were a tantalizing hint of the iteration that was to come, but the
solution was still just straight linear logic.
The next four tests pass outright, even the test for 8:
In some ways, this is a shame since it was the test for 8 that caused us
to transform an if to a while in the Java solution. No such elegant
transformation takes place in the Clojure solution; though I have to
say that the recursion is the better solution—so far.
61
Chapter 6 Prime Factors
Next comes the test for 9. And here the Java and Clojure versions face
the similar dilemma of duplicated code:
This solution is not sustainable. It would force us to add the 5, 7, 11, 13…
cases all the way up to the maximum prime that our language could hold.
But this solution does imply an interesting iterative/recursive solution:
The loop function creates a new anonymous function in situ. The recur
function, when nested inside a loop expression, causes the in situ function
to be reinvoked with TCO. The arguments to the in situ function are n,
divisor, and factors. Each is followed by its initializer. So the n within the
loop is initialized to the value of n outside the loop (the two n identifiers
are distinct), divisor is initialized to 2, and factors is initialized to [].
The recursion in this solution is iterative because the recursive calls are at
the tail. Note that the cons has been changed to a conj because the ordering
62
Conclusion
of the list construction has changed. The conj function appends6 to factors.
Convince yourself that you understand why the ordering has changed!
Conc lusion
There are several things to note about this example. First, the sequence of
tests is the same between the Java and Clojure versions. This is significant
because it implies that the change to functional programming has little to
no impact on the way we express our tests. Tests are somehow more
basic, more abstract, or more essential than the programming style.
Second, the solution strategy between the two deviated even before any
iteration was required. In Java, the test for 4 did not require iteration; but
in Clojure, it caused us to use recursion. This implies that recursion is
somehow more semantically essential than standard looping with while
statements.
The end result is an algorithm that is similar to the Java solution but has
at least one surprising difference: It is not a doubly nested loop. The Java
solution has one loop that increments the divisor and another that
repeatedly adds the current divisor as a factor. The Clojure solution
replaces that doubly nested loop with two independent recursions.
Which solution is better? The Java solution is a lot faster because Java
is a lot faster than Clojure. But otherwise, I see no particular benefit to
either. To those who know both languages well, neither is easier than the
63
Chapter 6 Prime Factors
However, this is the last example for which the results will be ambiguous.
As we proceed from example to example, the differences will become
more and more significant.
64
7
Bowling Game
65
Chapter 7 Bowling Game
Now let’s look at another traditional TDD exercise: the Bowling Game
kata. What follows is a much-abbreviated version of that kata that
appeared in Clean Craftsmanship.1 A related video, Bowling Game, is also
available. You can access the video by registering at https://informit.com
/functionaldesign.
J ava Ve r sion
We begin, as always, with a test that does nothing, just to prove we can
compile and execute:
@Test
public void canCreateGame() throws Exception {
Game g = new Game();
}
And then we make that compile and pass by directing the integrated
development environment (IDE) to create the missing class:
@Test
public void canRoll() throws Exception {
Game g = new Game();
66
Java Version
g.roll(0);
}
And then we make that compile and pass by directing the IDE to create
the roll function, and we give the argument a reasonable name:
There’s a bit of duplication in the tests already. We should get rid of it. So
we factor out the creation of the game into the setup function:
@Before
public void setUp() throws Exception {
g = new Game();
}
}
This makes the first test completely empty. So we delete it. The second test
is also pretty useless since it doesn’t assert anything, so we delete it as well.
Next, we want to assert that we can score a game. But to do that we need
to roll a complete game:
@Test
public void gutterGame() throws Exception {
for (int i=0; i<20; i++)
g.roll(0);
assertEquals(0, g.score());
}
67
Chapter 7 Bowling Game
@Test
public void allOnes() throws Exception {
for (int i=0; i<20; i++)
g.roll(1);
assertEquals(20, g.score());
}
@Before
public void setUp() throws Exception {
g = new Game();
}
68
Java Version
@Test
public void gutterGame() throws Exception {
rollMany(20, 0);
assertEquals(0, g.score());
}
@Test
public void allOnes() throws Exception {
rollMany(20, 1);
assertEquals(20, g.score());
}
}
OK, next test. One spare, with one extra bonus ball, and all the rest
gutter balls:
@Test
public void oneSpare() throws Exception {
rollMany(2, 5);
g.roll(7);
rollMany(17, 0);
assertEquals(24, g.score());
}
This test fails, of course. We have to refactor the algorithm in order to get
this to pass. We move the computation of the score out of the roll method
and into the score method, and we walk through the rolls array two balls
(one frame) at a time:
69
Chapter 7 Bowling Game
frameIndex += 2;
}
}
return score;
}
@Test
public void oneStrike() throws Exception {
g.roll(10);
g.roll(2);
g.roll(3);
rollMany(16, 0);
assertEquals(20, g.score());
}
70
Clojure Version
}
return score;
}
@Test
public void perfectGame() throws Exception {
rollMany(12, 10);
assertEquals(300, g.score());
}
C loju r e Ve r sion
Things start out quite differently in Clojure. We have no classes to create,
and there is no need for a roll method. So our first test is the gutter
game:
2. The repeat function returns a sequence of repeating values. In this case, it is a sequence of
20 zeros.
71
Chapter 7 Bowling Game
To make this pass, we go through several steps. The first is to break the
rolls array up into frames and sum up the frames. At first, we assume
that frames have just two rolls:
Now the reduce function has come into its own. It cycles through the
pairs of rolls, accumulating them into a score.
This change keeps all the previous tests passing, but it still fails the spare
test. To pass that we have to add special processing to the to-frames and
add-frame functions. Our goal is to put all the rolls needed to calculate a
frame into the frame data.
3. You will want to look this function up. It does much more than this paragraph suggests. But you’ll
see that soon enough.
4. The partition function breaks the rolls list into a list of pairs. So [1 2 3 4 5 6] becomes
[[1 2][3 4][5 6]].
72
Clojure Version
Look closely at this code. There are lots of little tricks and workarounds
in it. Why? Because Clojure is full of lots of lovely, tempting little tools
that you can use to get data into almost the form you want, and then use
little tricks to maneuver the data into exactly the form you want. If you
aren’t careful, those little tricks can start to dominate the code.
So, for example, see if you can figure out why I am passing [[0]] into the
concat function in to-frames.8 As another example, ask yourself why I
used #(take 1 %) instead of just first.9
Because of the trickiness in this code, don’t be too concerned if you are
struggling to understand it. I struggled too when looking back over it.
And so…
5. The #(…) form creates an anonymous function. The % symbol is the argument to that function. You
can also use %n, where n is an integer representing the nth argument. So #(take 1 %) is a function
that returns a list containing the first element of its argument.
6. This is not a reassignment, or even a reinitialization. The second possible-bonuses value is dis-
tinct from the first. Think of it like a local variable in Java hiding a function argument or a mem-
ber variable of the same name.
7. The concat function concatenates lists together. So (concat [1 2] [3 4]) returns [1 2 3 4].
8. Since bonuses are based on the next frame, possible-bonuses had one too few elements. That
would have terminated the final call to map one element too early.
9. (take 1 x) returns a list containing the first element in x. first returns the first element.
73
Chapter 7 Bowling Game
When these little tricks proliferate it’s time to rethink the solution. So I
refactored the solution into a simple loop:
This is looking a lot better. Moreover, it’s starting to look a bit like the
Java solution. The next test is one strike:
And we make that pass by adding one more case to the cond:
(= 10 (first remaining-rolls))
74
Conclusion
Trivial, right? So all that’s left is the perfect game. And if this goes like the
Java version, this test should just pass without modification:
But it doesn’t! Can you see why? Perhaps the fix will elucidate that for you:
The to-frames function happily creates more than ten frames. It just runs
to the end of the rolls list making as many frames as it can. But a game
of bowling is only ten frames.
Conc lusion
There are quite a few interesting differences between the Java and Clojure
versions of this problem. First, the Clojure version has no Game class. So
all the machinations we used to create that class in the Java version
simply don’t occur in the Clojure version.
75
Chapter 7 Bowling Game
You might think that the loss of the Game class is a weakness of the
Clojure version. After all, it’s convenient to be able to just create a Game,
toss it a bunch of rolls, and then get the score. However, the Clojure
version has decoupled the accumulation of the rolls from the
computation of the score. Those concepts are not bound together in the
Clojure version. And that makes me think that the Java version has a
subtle violation of the Single Responsibility Principle.10
Second, as we tried to solve the one spare case, we saw how the Clojure
version got polluted with all those nasty little tricks. This is a real problem
with Clojure programs (or perhaps Clojure programmers). It’s just too
easy to add one more nasty little trick to get things to work.
Third, the Clojure solution is significantly different from the Java solution.
Oh, there are some points of similarity, to be sure. That cond structure in
the Clojure version is very reminiscent of the if/else structure in the Java
version. However, those two similar structures produced radically different
results. The Java version produced the score. The Clojure version produced
a frame that included the bonus balls for spares and strikes.
Which of these versions is better? The Java version ended up a bit simpler
than the Clojure version; but it was also a bit more coupled. The separation
of concerns in the Clojure version convinces me that between the two, it
would be more flexible and useful.
76
8
Gossiping B us
D rivers
77
Chapter 8 Gossiping Bus Drivers
Object orientation was born in 1966 when Ole-Johan Dahl and Kristen
Nygaard added some modifications to the ALGOL-60 language in order
to make the language more amenable to discrete event simulation.1 The
new language was called SIMULA-67 and is considered to be the first
true OO programming language.
Given n drivers, each with their own circular route of stops, determine
how many steps are required until all gossip known to each bus driver is
known by all. Drivers only gossip if they arrive together at the same stop.
So, let’s say that Bob knows rumor X and drives route [p,q,r]. Jim knows
rumor Y and drives route [s,t,u,p]. When will Bob and Jim be able to
share their gossip? If they start at time 0, then at time 3 they will both be
at stop p; remember, the routes are circular.
This problem gets more interesting when there are more than two drivers
and more complex routes.
J ava S olution
I wrote a solution to this problem in Java. I started out with a very
traditional kind of OO analysis and design (see Figure 8.1).
78
Java Solution
The Simulator holds many Drivers. Each Driver has a Route, and each
Route contains many Stops. Each Stop has many Drivers, and each Driver
has many Rumors.
*
* *
Rumor Stop
This is a fairly simple object model. There’s not even any inheritance
or polymorphism implied. So it should be a pretty straightforward
implementation.
I wrote the Java code using TDD, of course. Here are the tests. As you
can see, they are fairly wordy; but at least they all fit into a single test
class:3.
package gossipingBusDrivers;
import org.junit.Before;
import org.junit.Test;
3. If you read my book Clean Craftsmanship (Addison-Wesley, 2021), you’ll understand why this is a
good thing.
79
Chapter 8 Gossiping Bus Drivers
@Before
public void setUp() {
stop1 = new Stop("stop1");
stop2 = new Stop("stop2");
stop3 = new Stop("stop3");
route1 = new Route(stop1, stop2);
route2 = new Route(stop1, stop2, stop3);
rumor1 = new Rumor("Rumor1");
rumor2 = new Rumor("Rumor2");
rumor3 = new Rumor("Rumor3");
driver1 = new Driver("Driver1", route1, rumor1);
driver2 = new Driver("Driver2", route2, rumor2, rumor3);
}
@Test
public void driverStartsAtFirstStopInRoute() throws Exception {
assertEquals(stop1, driver1.getStop());
}
@Test
public void driverDrivesToNextStop() throws Exception {
driver1.drive();
assertEquals(stop2, driver1.getStop());
}
@Test
public void driverReturnsToStartAfterLastStop()
throws Exception {
80
Java Solution
driver1.drive();
driver1.drive();
assertEquals(stop1, driver1.getStop());
}
@Test
public void firstStopHasDriversAtStart() throws Exception {
assertThat(stop1.getDrivers(), containsInAnyOrder(driver1,
driver2));
assertThat(stop2.getDrivers(), empty());
}
@Test
public void multipleDriversEnterAndLeaveStops()
throws Exception {
assertThat(stop1.getDrivers(), containsInAnyOrder(driver1,
driver2));
assertThat(stop2.getDrivers(), empty());
assertThat(stop3.getDrivers(), empty());
driver1.drive();
driver2.drive();
assertThat(stop1.getDrivers(), empty());
assertThat(stop2.getDrivers(), containsInAnyOrder(driver1,
driver2));
assertThat(stop3.getDrivers(), empty());
driver1.drive();
driver2.drive();
assertThat(stop1.getDrivers(), containsInAnyOrder(driver1));
assertThat(stop2.getDrivers(), empty());
assertThat(stop3.getDrivers(), containsInAnyOrder(driver2));
driver1.drive();
driver2.drive();
assertThat(stop1.getDrivers(), containsInAnyOrder(driver2));
assertThat(stop2.getDrivers(), containsInAnyOrder(driver1));
assertThat(stop3.getDrivers(), empty());
}
@Test
public void driversHaveRumorsAtStart() throws Exception {
81
Chapter 8 Gossiping Bus Drivers
assertThat(driver1.getRumors(), containsInAnyOrder(rumor1));
assertThat(driver2.getRumors(), containsInAnyOrder(rumor2,
rumor3));
}
@Test
public void noDriversGossipAtEmptyStop() throws Exception {
stop2.gossip();
assertThat(driver1.getRumors(), containsInAnyOrder(rumor1));
assertThat(driver2.getRumors(), containsInAnyOrder(rumor2,
rumor3));
}
@Test
public void driversGossipAtStop() throws Exception {
stop1.gossip();
assertThat(driver1.getRumors(), containsInAnyOrder(rumor1,
rumor2,
rumor3));
assertThat(driver2.getRumors(), containsInAnyOrder(rumor1,
rumor2,
rumor3));
}
@Test
public void gossipIsNotDuplicated() throws Exception {
stop1.gossip();
stop1.gossip();
assertThat(driver1.getRumors(), containsInAnyOrder(rumor1,
rumor2,
rumor3));
assertThat(driver2.getRumors(), containsInAnyOrder(rumor1,
rumor2,
rumor3));
}
82
Java Solution
@Test
public void driveTillEqualTest() throws Exception {
assertEquals(1, Simulation.driveTillEqual(driver1,
driver2));
}
@Test
public void acceptanceTest1() throws Exception {
Stop s1 = new Stop("s1");
Stop s2 = new Stop("s2");
Stop s3 = new Stop("s3");
Stop s4 = new Stop("s4");
Stop s5 = new Stop("s5");
Route r1 = new Route(s3, s1, s2, s3);
Route r2 = new Route(s3, s2, s3, s1);
Route r3 = new Route(s4, s2, s3, s4, s5);
Driver d1 = new Driver("d1", r1, new Rumor("1"));
Driver d2 = new Driver("d2", r2, new Rumor("2"));
Driver d3 = new Driver("d3", r3, new Rumor("3"));
assertEquals(6, Simulation.driveTillEqual(d1, d2, d3));
}
@Test
public void acceptanceTest2() throws Exception {
Stop s1 = new Stop("s1");
Stop s2 = new Stop("s2");
Stop s5 = new Stop("s5");
Stop s8 = new Stop("s8");
Route r1 = new Route(s2, s1, s2);
Route r2 = new Route(s5, s2, s8);
Driver d1 = new Driver("d1", r1, new Rumor("1"));
Driver d2 = new Driver("d2", r2, new Rumor("2"));
assertEquals(480, Simulation.driveTillEqual(d1, d2));
}
}
83
Chapter 8 Gossiping Bus Drivers
D r iv e r
package gossipingBusDrivers;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
84
Java Solution
Route
package gossipingBusDrivers;
Stop
package gossipingBusDrivers;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
85
Chapter 8 Gossiping Bus Drivers
R u mor
package gossipingBusDrivers;
86
Java Solution
S im u l ation
package gossipingBusDrivers;
import java.util.HashSet;
import java.util.Set;
87
Chapter 8 Gossiping Bus Drivers
A quick perusal of this code will convince you that it is written in a very
traditional OO style and that the objects encapsulate their own state
relatively well.
C lojur e
When writing the Clojure version I did not start out with a design sketch.
Rather, I depended upon my TDD tests to help me with the design. The
tests are as follows:
(ns gossiping-bus-drivers-clojure.core-spec
(:require [speclj.core :refer :all]
[gossiping-bus-drivers-clojure.core :refer :all]))
4. #{. . .} represents a set in Clojure. A set is a list of items that has no duplicates.
88
Clojure
89
Chapter 8 Gossiping Bus Drivers
There are some interesting similarities between the Java tests and the
Clojure tests. They are both quite wordy; although the Clojure tests
contain half as many lines. The Java version has 12 tests whereas the
Clojure version has only 8. This difference has a lot to do with the way
the two different solutions were partitioned. The Clojure tests also play
pretty fast and loose with the data.
(ns gossiping-bus-drivers-clojure.core
(:require [clojure.set :as set]))
90
Clojure
7. The update function returns a new map with one element changed. (update m k f a) changes
the k element of m by applying the function (f e a), where e is the old value of element k. Thus,
(update {:x 1} :x inc) returns {:x 2}.
8. The union function is from the set namespace. Notice the ns at the top aliases the clojure.set
namespace to just set.
9. vals returns a list of all the values in a map. keys returns a list of all the keys in a map.
10. The flatten function turns a list of lists into a list of all the elements. So (flatten [[1 2][3 4]])
returns [1 2 3 4].
91
Chapter 8 Gossiping Bus Drivers
time 1]
(cond
(> time 480) :never
(apply = (map :rumors world)) time
:else (recur (drive world) (inc time)))))
This solution is 42 lines, whereas the Java solution is 145 lines spread
among five files.
Six out of the eight functions take only the world as an argument. Thus,
we might say that the only true object in this system is the world, and it
has five methods. But even that is a stretch.
Even if we decide that the Driver is a kind of object, it is not mutable. The
simulated world is nothing more than a list of immutable Drivers. The
drive function accepts the world and produces a new world in which all
the Drivers have been moved one step, and Rumors have been spread at
any stop where more than one Driver has arrived.
92
Conclusion
This tells us that the partitioning of this system is not about objects,
but about functions. The relatively unpartitioned data passes from one
independent function to the next.
You might argue that the Java code is relatively straightforward, whereas
the Clojure code is too dense and obscure. Believe me when I say that it
does not take very long to get comfortable with that density and that the
perceived obscuration is an illusion based on unfamiliarity.
On the other hand, the dividing lines we chose for the Java version are not
guaranteed to lead to an effective partitioning. The warning is in the drive
function of the Clojure program. It seems likely that a better partitioning
of this system might lie along the different operations that manipulate the
world, rather than things like Routes, Stops, and Rumors.
Conc lusion
We saw some differences in the Prime Factors and Bowling Game katas;
but the differences were relatively minor. The differences in the Gossiping
Bus Drivers kata were much more pronounced. This is likely because that
last kata was a bit larger than the first two (I’d say twice the size), and
also because it was a true finite state machine.
A finite state machine moves from state to state, taking actions that depend
upon the incoming events and the current state. When such systems are
written in an OO style, the state tends to be stored in mutable objects
that have dedicated methods. But in a functional style, the state remains
93
Chapter 8 Gossiping Bus Drivers
94
9
O bject- O riented
Progr amming
95
Chapter 9 Object-Oriented Programming
In Clean Architecture,1 I made the point that the OO style has three
attributes: encapsulation, inheritance, and polymorphism. I then led you
through the reasoning that, of the three, polymorphism is the most
beneficial. The other two are, at best, ancillary.
The examples in the previous chapters did not lend themselves to any
polymorphism. Let’s correct that by examining how we might solve
the Payroll problem from Section 3 of Agile Software Development:
Principles, Patterns, and Practices.2
96
Object-Oriented Programming
that sum is greater than 40, the remaining hours are paid at 1.5 times
their hourly rate.
• Employees are given the option to have their paychecks mailed to their
home address, held at their paymaster’s office, or directly deposited
into their bank account. The address, paymaster, and bank information
are fields in their employee record.
Payroll * Employee I
Pay
+ isPayDay Classification
+ calcPay
+ sendPay + calcPay
I I
Pay Pay
Disposition Schedule Hourly Commissioned Salaried
+ sendPay + isPayDay
* *
Timecard Sales Receipt
Mail
Weekly
– addr
Hold
Bi-weekly
– paymaster
Deposit
Monthly
– account
Perhaps the best place to begin is with the Payroll class. In Java, it has a
run method that looks like this:
void run() {
for (Employee e : db.getEmployees()) {
97
Chapter 9 Object-Oriented Programming
if (e.isPayDay()) {
Pay pay = e.calcPay();
e.sendPay(pay);
}
}
}
I have made the point many times, and in many places, including the
aforementioned books, that this little snippet of code is the pure truth.
For each employee, if today is the day they should be paid, then calculate
their pay and send it to them.
From that little snippet of code, the rest of the implementation ought
to be pretty clear. There are three uses of the Strategy3 pattern: one to
implement calcPay, another to implement isPayDay, and the last
to implement sendPay.
There is also a very clear architectural boundary (dashed line) that cuts
across all those inheritance relationships, dividing the high-level abstractions
from the low-level details.
3. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of
Reusable Object-Oriented Software (Addison-Wesley, 1995), 315.
98
Functional Payroll
Get
Paycheck
amounts
Employees Amounts
Get
Get
Employees Get IDs
Paycheck
to be Paid IDs Directives
Today
Employees
Paycheck
Get Directives
Employees Get Dispositions
Dispositions
DB
Still, the DFD helps us propose the functional version of the pure truth:
99
Chapter 9 Object-Oriented Programming
Notice that this differs from the Java version in that it is not an iterative
approach. Rather, the list of employees flows through the program,
getting modified at each stage according to the data flow diagram. This
is typical of the way functional programs are conceived and written.
Functional programs tend to be more like plumbing than step-by-step
procedures. They regulate and modify the flow of data, rather than
iterating step by step through the data.
So, what about the architecture? There was that nice architectural
boundary in the UML diagram of the OO version. Where is the
architectural boundary in the functional version?
Let’s look a bit deeper. The tests may give us some hints:
In this test, the database contains a list of employees, and each employee
is a hash map with specific fields. That’s not so different from an object,
is it? The payroll function returns a list of paycheck directives, each of
which is also a hash map—another object. Interesting.
100
Functional Payroll
:schedule :weekly
:pay-class [:hourly 15]
:disposition [:deposit "routing" "account"]}]
time-cards {"empid" [["Nov 12 2022" 80/104]]}
db {:employees employees :time-cards time-cards}
friday (parse-date "Nov 18 2022")]
(should= [{:type :deposit
:id "empid"
:routing "routing"
:account "account"
:amount 120}]
(payroll friday db))))
This test shows how the employee and paycheck-directive objects vary
based upon the :schedule, :pay-class, and :disposition. It also shows
that the database contains time-cards associated with employee ids.
From this, the third test ought to be predictable:
Notice that the payments are being properly calculated, the dispositions
are being correctly interpreted, and—as far as we can tell—the schedules
are being followed. So how is this all being accomplished?
4. This is not 80 divided by 10. Rather, it is the rational number 80/10. This ensures that subsequent
mathematics will not treat the value as an integer.
101
Chapter 9 Object-Oriented Programming
5. (filter predicate list) calls predicate for every member of list and returns a sequence of
all the members for which predicate was not falsey.
6. The partial function takes a function and some arguments, and returns a new function in which all
those arguments have already been initialized. Thus, ((partial f 1) 2) is equivalent to (f 1 2).
7. In this case, the for function calls dispose for each paycheck-directive in the list returned by
create-paycheck-directives.
102
Functional Payroll
Do you see those defmulti statements (in bold)? They are analogous,
though not identical, to a Java interface. Each defmulti defines a
polymorphic function. However, that function does not dispatch based
upon an intrinsic type, the way Java or C# or even Ruby and Python do.
Rather, they dispatch upon the result of the function specified right after
the name.
So, the get-pay-class function returns the value that the calc-pay
function will polymorphically dispatch on. What does get-pay-class
return? It returns the first element of the pay-class field of the employee.
According to our tests, those values are :salaried, :hourly, and
:commissioned.
8. The trailing - makes this a private function, so only functions in this file can access it.
9. (get m k) returns the value of k in the map m.
103
Chapter 9 Object-Oriented Programming
Payroll.clj
requires PayrollTest
requires .clj
Payroll
Implementation.clj
10. Destructures the pay-class of the employee and ignores the first element.
104
Functional Payroll
The arrows depict the requires relationships between the source files. The
source code of those requires in the payroll-implementation.clj file
looks like this:
(ns payroll-implementation
(:require [payroll :refer [is-today-payday calc-pay dispose]]))
What does all this inversion mean? It means that the low-level details
in payroll-implementation.clj depend upon the high-level policy in
payroll.clj. And whenever low-level details depend upon high-level
policy, we have the potential for an architectural boundary. We could
even draw it as shown in Figure 9.4.
Payroll
Payroll
Implementation
Notice that I used a UML implements arrow. It’s almost as if Payroll and
PayrollImplementation were classes in a Java program.
105
Chapter 9 Object-Oriented Programming
But we can do even better than this. We can move all the defmulti
statements, along with their supporting functions, into their own
payroll-interface namespace and source file, like this:
(ns payroll-interface)
And now we can draw the architecture diagram as shown in Figure 9.5.
I
Payroll
Payroll
Interface
Payroll
Implementation
This is starting to look more and more like the UML diagram of a Java or
C# program. It looks like we got a Payroll class, a PayrollInterface
class, and a PayrollImplementation class. And indeed, from an architectural
point of view, that’s a pretty accurate statement.
But there are some interesting differences. Where, for example, are the
PaySchedule, PayClassification, and PayDisposition classes that we
saw in the UML of the OO Java program?
We could easily pull them out of the Clojure program by splitting the
PayrollImplementation.clj file into three namespaces and files, as shown
in Figure 9.6.
106
Namespaces and Source Files
I
Payroll
Payroll
Interface
This is not the kind of thing you can do in Java or C# since there is no way,
in those languages, to implement each function of an interface in a different
module. However, it’s perfectly possible in Clojure. The important thing to
remember is that this is an architectural diagram, not a class diagram.
PaySchedule, PayClassification, and PayDisposition are namespaces and
source files, not classes. We do not make instances of them. They don’t
represent objects in an OO sense.
Not that there aren’t objects in our Clojure solution. There certainly are.
The employee, the paycheck-directive, and even the pay-class and
disposition are objects. They do not have methods as strongly associated
with them as they would if they were written in an OO language; but
there are functions through which those objects flow.
107
Chapter 9 Object-Oriented Programming
So, when writing functional programs, it is not a bad idea to consider the
partitioning disciplines of OO and continue to apply them. We’ll see more
of this later as we investigate principles, patterns, and architecture.
Conc lusion
First of all, functional programs and OO programs are different. Functional
programs tend to be constructions of plumbing that regulate data flow
transformations, while mutable OO programs tend to iterate step by step
over objects. However, from an architectural point of view, the two styles
are quite compatible. It turns out that we can partition the functions of a
functional program into the same kinds of architecturally significant
elements as an OO program. From an architectural point of view, there’s
very little difference.
We shall see as these pages turn more and more toward design and
architecture, the differences between functional programs and the object
orientation of immutable objects start to become less and less relevant.
108
10
Types
109
Chapter 10 Types
The preceding chapter may have left you somewhat distressed. Those things
that I called objects were just hash maps and were completely untyped.
Anybody could stick anything into them without any constraint. The salary
in the :pay-class could hold a string instead of a number. The :schedule
field could hold an integer instead of the appropriate keyword.
In short, these objects are not statically typed. The compiler does not
check them. And therefore, all hell could break loose!
Those of us who practice TDD are not usually very concerned about that
hell. Our tests generally ensure that the objects that we pass around are
properly constructed. Still, in complex systems, where the totality of all the
objects can end up being quite complex, there is a need for a more formal
and complete way to ensure the integrity of our types than a dynamically
typed language (and even most statically typed languages) can give us.
110
Types
string? string?))
(s/def ::paymaster-disposition (s/tuple #(= % :paymaster)
string?))
(s/def ::disposition (s/or :mail ::mail-disposition
:deposit ::deposit-disposition
:paymaster ::paymaster-disposition))
111
Chapter 10 Types
If this looks scary, it should. There’s a lot of detail in there. Keep in mind,
however, that this is the level of detail that you would have to specify
within the modules of a statically typed language in order to capture all
the type constraints.
If you look a bit higher in the specification, you’ll see that ::employees
is just a collection of ::employee, ::sales-receipts is a collection of
::sales-receipt, and ::time-cards is a collection of ::time-card. Don’t
let the double colons bother you; they are a namespace convention. You
can read the Clojure docs later if you want to understand them. For now,
just look at the keywords and ignore how many colons there are.
112
Types
The s/or statements might bother you a bit. The arguments come in pairs,
and the first in each pair is just the name of that alternative. So, in the
::disposition definition, :mail is just the name of the ::mail-disposition
alternative. Don’t worry anymore about this. It will become clear if you
decide one day to read the clojure.spec docs.
So, given this elaborate type specification, how do we use it? I sometimes
use it in my tests as follows:
Look for the calls to s/valid?, which is a function that returns true if the
data matches the spec. Look carefully and you’ll see that I’m checking the
::db spec on the way in and the ::paycheck-directives spec on the way
out. This is pretty secure. If my tests have high coverage, and they all
check the specs for the inputs and outputs of the functions they call, then
violations of type ought to be extremely rare.
I have, upon occasion, also used Clojure’s :pre and :post features to run
the specs on critical data before and after the main processing functions
of my applications.
113
Chapter 10 Types
Here, for example, is the main processing step of the spacewar2 game I
wrote some years ago:
The :pre and :post statements are commented out,3 but they are ready to
be reasserted should I suspect some kind of terrible type corruption.
Conc lusion
There is a lot of wailing and gnashing of teeth over the static versus
dynamic typing issue. Each side yells at the other without listening to what
either side has to say. I think both sides have valid points. Dynamic typing
makes code easier to write. Static typing makes code a lot safer, easier to
understand, and much more internally consistent. It seems to me that a
library like clojure.spec strikes a great balance. It gives you the ability to
have as much or as little type checking as you need. It allows you to specify
when types are checked and when they are not. What’s more, it allows you
to specify dynamic constraints that no static type system can check. So, for
my money, libraries like this give you better than the best of both worlds.
2. https://github.com/unclebob/spacewar
3. I don’t much care for commented-out code. I’d remove these lines as the project matured.
114
III
Functional D esign
115
This page intentionally left blank
11
Data Flow
117
Chapter 11 Data Flow
We can see this bias in many of our previous examples, including the
Bowling Game, Gossiping Bus Drivers, and Payroll applications in Part II,
Comparative Analysis.
This is actually quite typical of the way old CRT2 displays used to work.
You had to energize the electron beam at just the right moment as it
rastered over the screen. So you matched the bits in the bitmap to the
clock that drove that beam. If, according to the clock, the beam was
at position 934, and if the 934th bit in the bitmap was set, then you
energized the beam for an instant to display that pixel.
1. https://adventofcode.com/2022/day/10
2. Cathode ray tube. A cathode ray is an electron. CRTs have electron guns that create narrow beams
of electrons that are rastered across the screen using regularly changing magnetic fields. The beam
strikes phosphors on the screen and makes them glow, thus creating a raster image.
118
Data Flow
the screen would be visible for a clock cycle if, and only if, at the beginning
of that cycle the x register matched the clock cycle number.
So if according to the clock, the beam was over screen position 23, and
if the x register was 23 at the start of cycle 23, then the beam would be
energized for that clock cycle.
Since the screen is 40 pixels wide and 6 pixels tall, the matching of the
clock cycle to x is modulus 40.
The task was to execute a set of instructions and produce a list of six
strings that were 40 characters each, with "#" indicating a pixel that was
visible and "." indicating one that was not visible.
If you were to write this program in Java, C, Go, C++, C#, or any other
procedural/OO language, you might create a loop that iterated one cycle
at a time while accumulating the appropriate pixels for each cycle. The
loop would consume instructions and modify the x register as directed.
package crt;
119
Chapter 11 Data Flow
public Crt(int x) {
this.x = x;
}
120
Data Flow
Notice all the mutated state. Notice how it iterates, cycle by cycle, to
populate the pixels. Notice also the funny business of extraCycles
to account for the fact that addx takes two cycles to execute.
Finally, notice that although the program is nicely partitioned into a few
smallish functions, those functions are all coupled together by the mutable
state variables. That is, of course, the usual situation for methods of a
mutable class.
I solved this problem in Clojure today. And the solution I came up with was
very different from the Java code above. Remember as you read this to start
at the bottom. Clojure programs are always written from the bottom up.
(ns day10-cathode-ray-tube.core
(:require [clojure.string :as string]))
121
Chapter 11 Data Flow
(noop state)
(if-let [[_ n] (re-matches
#"addx (-?\d+)" line)]
(addx (Integer/parseInt n) state)
"TILT"))]3
(recur state (rest lines)))))
(defn -main []
(-> "input"
execute-file
render-cycles
print-screen))
3. TILT is my favorite error message. Long ago, pinball machines would put up this message and can-
cel your game if you physically tilted the machine in order to manipulate the ball.
122
Data Flow
Notice that there are, of course, no mutable variables. Instead, the state
value flows through each of the functions as though through a pipeline.
The state value begins in execute-file and then flows to execute, then
repeatedly to noop or addx, and then back to execute, and finally back to
execute-file. At each stage in that flow, a new value of state is created
from the old without changing the old.
If this seems eerily familiar to you, it should. This is very much like the
pipes and filters we have gotten used to in our command-line shells. Data
flows into a command from a pipe, is transformed by that command, and
then flows out to the next command through a pipe.
It lists the private/messages directory and then cuts out certain fields.
The data flows out of the ls command, through the pipe, and then into
the cut command. This has the same kind of feel as the state value
flowing through the execute, addx, and noop functions.
Finally, notice that there is none of the funny business we saw in the Java
program surrounding the two cycles of the addx instruction. Instead, the
123
Chapter 11 Data Flow
two cycles are neatly accounted for by simply adding two x values to the
:cycles element of the state.
Of course, I didn’t have to use the data flow style. I could have created a
Clojure algorithm that was much closer to the Java algorithm. But that’s
not the way I think about things when I’m writing in a functional language.
Instead, I am biased toward data flow solutions.
Some of the newer features in Java and C# lend themselves to the data flow
style. But they are wordy and appear to me to be bolted onto the languages
in awkward ways. Your mileage may vary; but I find that when I use
procedural/OO languages I tend to iterate much more than I tend to plumb.
124
12
SOLID
125
Chapter 12 SOLID
I wrote about the SOLID principles over two decades ago in the context
of OO design. Because of that context, many have come to associate
those principles with OO and regard them as anathema to functional
programming. This is unfortunate because the SOLID principles are
general principles of software design that are not specific to any
particular programming style. In this chapter, I will endeavor to explain
how the SOLID principles apply to functional programming.
The SRP is a simple statement about focusing our modules on the sources
that cause them to change. Those sources are, of course, people. It is
126
The Single Responsibility Principle (SRP)
These people can be separated into groups called roles or actors. An actor
is a person, or a group of people, who require the same things from the
system. The kinds of changes they request will be consistent with each
other. On the other hand, different actors have different needs. The
changes one actor requests will affect the system in very different ways
from the changes requested by other actors. Those disparate changes may
even be at cross purposes to each other.
127
Chapter 12 SOLID
:credit-limit 50000}
(parse-customer
["Customer-id: 1234567"
"Name: customer name"
"Address: customer address"
"Credit Limit: 50000"])))
128
The Single Responsibility Principle (SRP)
The first test tells us that we are parsing some text input into a customer
record. That record has four fields: id, name, address, and credit-limit.
The next four tests tell us about syntax errors such as missing or malformed
input.
The last test is the interesting one. It tests a business rule. Testing a
business rule as part of parsing the input is a clear SRP violation. The
parsing code can safely validate syntax errors, but it should avoid all
semantic checks because those checks are in the domain of a different
actor. The actor who specifies the input format is not the same as
the actor who specifies the largest allowable credit limit.3
(defn validate-customer
[{:keys [id name address credit-limit] :as customer}]
(if (or (nil? id)
(nil? name)
(nil? address)
(nil? credit-limit))
:invalid
(let [credit-limit (Integer/parseInt credit-limit)]
(if (> credit-limit 50000)
:invalid
(assoc customer :credit-limit credit-limit)))))
3. This is true even when the two actors are the same person. In that case, that person is playing two
different roles.
129
Chapter 12 SOLID
(validate-customer
{:id id
:name name
:address address
:credit-limit credit-limit})))
Why am I concerned about mixing the credit limit constraint with the
syntax of the data structure? It is because I expect the syntax of the data
structure and the credit limit constraint to be specified by different actors.
And I expect those different actors will request changes at different times
and for different reasons. I don’t want a change to the syntax to
inadvertently break a business rule.
130
The Open-Closed Principle (OCP)
there is a business rule that says that credit limits must not exceed
50,000, then the enforcement code should go in the module that handles
all the other credit limit processing.
The OCP was first stated by Bertrand Meyer in his classic 1988 book,
Object-Oriented Software Construction.4 To paraphrase, it says that
software modules should be open for extension but closed for modification.
This means that you want to design your modules such that extending or
changing their behavior does not require you to modify their code.
This may sound oxymoronic, but it’s actually something that we do all
the time. Consider, for example, the copy program in C:
void copy() {
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
4. Pearson, 1988.
131
Chapter 12 SOLID
This program copies characters from stdin to stdout. I can add new
devices to the operating system anytime I like. For example, I could add
an optical character recognition (OCR) and a text-to-speech synthesizer
to the system. This program would still operate without complaint and
would happily copy characters from the OCR to the voice synthesizer
without needing to be modified or even recompiled.
5. The keyword interface in Java and C# defines classes where every method is abstract.
132
The Open-Closed Principle (OCP)
Fu nction s
Consider this simple Clojure program:
By the way, I tested this program using the following tests. I think you’ll
find this interesting.
(defn str-read []
(let [c (first @str-in)]
(if (nil? c)
:eof
(do
(swap! str-in rest)
c))))
(describe "copy"
(it "can read and write using str-read and str-write"
6. Functions that are passed as arguments, or returned as values from functions, are sometimes called
higher-order functions.
133
Chapter 12 SOLID
I used the atoms because I/O is a side effect and is therefore not purely
functional. After all, when you read from an input or write to an output,
you are mutating their states. Thus, the low-level I/O functions are not
purely functional and use Software Transactional Memory to manage the
mutation of state.
The test simply loads the device map with the functions:
134
The Open-Closed Principle (OCP)
M u lti - m ethods
Still another variation on this theme is the use of multi-methods. Many
languages, functional or otherwise, support multi-methods in one way or
another. Multi-methods are another form of duck typing, because they
create a loose grouping of methods that are dynamically dispatched based
on their function signature and the “type”7 of the arguments.
The test for this new copy function is below. Notice that the test device
is no longer a vtable containing pointers to functions. Instead, it now
contains the input and output atoms, and also a :device-type. It is that
:device-type that the multi-methods will be dispatching on.
7. I used quotes here because the “type” of the arguments is not necessarily associated with their spe-
cific data types. Indeed, that “type” can be a completely different concept.
135
Chapter 12 SOLID
These are the implementations that will be dispatched when the :device-
type is :test-device. It should be clear that many other such implementation
methods could be created for various different devices. Those new devices
will extend the copy program without forcing any modification.
I n de pe n de nt D e ploya bilit y
One of the benefits we expect to get from the OCP is the ability to compile
high-level policies and low-level details in separate modules and to deploy
them independently. In Java and C#, this would mean compiling them
down into separate jar or dll files that can be dynamically loaded. In C++,
we would compile the modules and place the binaries into dynamically
loadable shared libraries.
The Clojure solutions shown above do not achieve that goal. The high-
level policy and the low-level detail cannot be dynamically loaded from
two separate jar files.
136
The Open-Closed Principle (OCP)
(defprotocol device
(getchar [_])
(putchar [_ c]))
(putchar [_ c]
(swap! out-atom str c)))
(describe "copy"
(it "can read and write using str-read and str-write"
(let [device (->str-device (atom "abcdef") (atom nil))]
137
Chapter 12 SOLID
(copy device)
(should= "abcdef" @(:out-atom device)))))
Notice the ->str-device function in the test. That’s essentially the Java
constructor of the str-device class that implements the device protocol.
Notice also that I loaded the atoms into the device as in the previous
example.
Indeed, I did not change the copy program to get this example to work.
The copy program is exactly as it was in the multi-method example. Now
that’s the OCP at work!
Any language that supports the OCP must also support the LSP. The
two principles are linked because every violation of the LSP is a latent
violation of the OCP.
138
The Liskov Substitution Principle (LSP)
The LSP was first described by Barbara Liskov in 1988,9 providing a more or
less formal definition of a subtype. In essence, she said that a subtype must
be substitutable for its base type in any program that uses the base type.
To clarify that, let us say that we have some program pay that uses a type
employee:
Notice that I’m using the vtable approach to create the type. Notice also
that the data within the type is completely hidden from the pay function.
All the pay function can see is the methods within the employee type.
How much more OO can you get?
Here’s the test code that uses this type. Notice that the make-test-employee
function makes an object that uses duck typing to conform to the employee
type:
9. Coincidentally, that’s the same year that Bertrand Meyer published the OCP.
139
Chapter 12 SOLID
(describe "Payroll"
(it "pays a salaried employee"
(should= "Send 100 to: name at: address"
(pay (make-test-employee "name" "address" 100)
:now))))
However, to achieve that I must be very careful to make sure that every
employee object I create conforms to the expectations of the pay function.
If one of those methods does something that pay doesn’t expect, then pay
will malfunction.
10. Holding all the data behind a single field to help keep it private. See https://cpppatterns.com
/patterns/pimpl.html.
140
The Liskov Substitution Principle (LSP)
Now imagine you were the author of the pay function, and you were
tasked with debugging why certain employees were getting paychecks
at the wrong times. You find that many employee objects are using the
:tomorrow convention instead of returning a boolean as they should.
What do you do?11
You could fix all those employees. Or you could add an extra condition
to the pay function:
11. Of course, a statically typed language would solve that particular issue. So would a well-timed
call to s/valid?, given appropriate specs. But that’s not the case we are investigating at the
moment.
141
Chapter 12 SOLID
Yeah, that’s pretty ugly.12 It’s also an OCP violation because we’ve
modified high-level policy due to the misbehavior of a low-level detail.
The I SA R u le
The OO literature often uses the term ISA (pronounced, and meaning,
“is a”) to describe subtypes. To describe the above situation in those
terms we would say that the test-employee ISA employee, and the
later-employee ISA employee. This usage can be confusing.
But second, and perhaps more important, the term ISA can be deeply
misleading. The ancient and venerable square/rectangle conundrum is
often used to make this point.
(defn make-rect [h w]
{:h h :w w})
12. Think long and hard about why that is ugly and why many programmers would be tempted to
delete the = true, thus re-exposing the bug.
142
The Liskov Substitution Principle (LSP)
To make this work we’ll need the set-h, set-w, and area functions as follows:
So let’s flesh this out a bit and create a small system that uses our
rectangle. Here are the tests:
(describe "Rectangle"
(it "calculates proper area and perimeter"
(should= 25 (area (make-rect 5 5)))
(should= 18 (perimeter (make-rect 4 5)))
(should= 12 (-> (make-rect 1 1) (set-h 3) (set-w 4) area)))
13. This destructures the map into the named components. In this case, it is equivalent to (let [h
(:h rect) w (:w rect)]…
143
Chapter 12 SOLID
Again, there’s nothing very surprising about this. Perhaps you are
confused by the minimally-increase-area function. This function simply
increases the area of the rectangle by the smallest integral amount
possible.14
So now let’s imagine that this system has been in operation for years and
has been very successful. But lately the customers of this system have
been asking for squares. How do we add squares to our system?
If we apply the ISA rule, we might decide that a square is a rectangle, and
therefore, we should make the functions that accept rectangles also accept
squares. In Java, we might accomplish this by deriving the class Square
from the class Rectangle. In Clojure, we can do this by simply creating
rectangles with equal sides:
This should bother us slightly because the size of the square object is the
same as the size of the rectangle object. Objects of type square ought to
be smaller since they don’t need both the height and the width. But
memory is cheap, and we want to keep things simple, right?
The question is, will all our tests still pass? They should, of course,
because our squares are really just rectangles (ah, that’s just the
ISA rule!).
144
The Liskov Substitution Principle (LSP)
The functions set-h and set-w do not return a square when passed a
square. That’s a bit strange; but in some bizarre way it actually makes
sense. I mean, if you set the height of a square without changing the
width, it’s not going to be a square anymore, right?
If you feel a little itching at the back of your brain right now, you should
probably pay attention to it.
Yes, that passes too. And of course, it should since the function simply
increases the height or width as necessary.
N ope !
Our customer calls us up a few days later, and he’s not very happy. He’s
been trying to minimally increase the area of his squares, and it’s just not
working.
145
Chapter 12 SOLID
We could add a :type field to the objects and have the constructors put
either :square or :rectangle into the field, respectively. And of course,
then we’d have to put an if statement into the minimally-increase-area
function. We’d also have to change set-h and set-w to change the type to
:rectangle. And those changes violate the OCP, because every violation
of the LSP is a latent violation of the OCP.
I’ll leave other solutions as an exercise. You might try using multi-methods.
You might try using protocols and records. You might try using vtables. Or
you might just keep the two types absolutely separate and never pass a
square into a function that takes a rectangle.
The R e pr e s e ntative R u le
I prefer this last option. That’s because I don’t much care for the ISA rule.
You see, while it is geometrically true that a square is a rectangle, none of
the objects in my code were actual rectangles or squares. My code had
objects that represented squares and rectangles, but they were neither
squares nor rectangles. And here’s the thing about representatives:
146
The Interface Segregation Principle (ISP)
When you see two objects in the real world that are obviously connected by
the phrase “is a,” you may be tempted to create a subtype relationship in
your code. Be careful with that. You may just run afoul of the representative
rule and violate the LSP.
The name of this principle derives from its origins in statically typed OO
languages. The example I usually use to describe the ISP works quite well
for such languages as Java, C#, and C++, because those languages depend
upon declared interfaces. In dynamically typed languages like Ruby,
Python, JavaScript, and Clojure, those examples don’t work particularly
well, because in those languages, interfaces are undeclared and are
already segregated by duck typing.
147
Chapter 12 SOLID
interface AtmInteractor {
void requestAccount();
void requestAmount();
void requestPin();
}
interface AccountInteractor {
void requestAccount();
}
interface AmountInteractor {
void requestAmount();
}
interface PinInteractor {
void requestPin();
}
Then each user can depend only upon the methods that it needs to call
while the implementation can multiply implement those interfaces:
148
The Interface Segregation Principle (ISP)
Perhaps the UML diagram in Figure 12.1 will make this clearer. By
segregating the interfaces, the three users depend only on the
methods that they need; and yet those methods can be implemented
by a single class.
I I I
Account Amount Pin
Interactor Interactor Interactor
ATM
Interactor
149
Chapter 12 SOLID
Dynamically typed languages like Ruby, Python, and Clojure do not have
this class-to-module constraint. You can declare anything you like within
any source file you like. You can write the entire application in a single
source file if you like!16 Therefore, it is even easier in those languages to
set up the conditions that will cause users of a module to depend upon
things they don’t need.
150
The Interface Segregation Principle (ISP)
Wh y?
Why do we care about depending on modules that have more than we
need? Why should it bother us if our module only uses one of the ten
functions in another module?
But possibly the best reason for caring about these dependencies is that a
module structure that limits extraneous dependencies is cogent. It is an
indication that intelligent human beings have cared enough to separate
the concerns and lower the coupling. The readers of your code will thank
you for that care.
Conc lusion
The real meaning of the ISP is:
151
Chapter 12 SOLID
Of the SOLID principles, one could say that the OCP is the moral heart,
the SRP is the organizing force, while the LSP and the ISP are caution
signs surrounding the potholes created by carelessness. That leaves the
DIP, which is the underlying mechanism behind all the others. In almost
every case when we find a principle violation, the solution involves the
inversion of one or more critical dependencies.
LL1 LL2 LL3 LL4 LL5 LL6 LL7 LL8 LL9 LL10 LL11 LL12
152
The Dependency Inversion Principle (DIP)
The dashed arrows are runtime dependencies. They show that high-level
modules call mid-level modules, which call low-level modules. The solid
arrows are source code dependencies. They show that each source code
module depends upon the modules it calls. Those source code dependencies
were statements like #include, import, require, and using that mentioned
the name of the downstream source file.
This meant that high-level policy was inextricably dependent upon low-
level detail. Think hard about the implications of that statement.
But in the late ‘60s, Ole-Johan Dahl and Kristen Nygaard moved a data
structure19 in the ALGOL compiler from the stack to the heap and
discovered OO.20 And with that discovery came the ability for programmers
to invert dependencies easily and safely.
18. Well, not quite always. In the late ‘50s and early ‘60s, Herculean efforts were expended by operat-
ing system engineers to invert a few, very strategic dependencies in order to create the abstraction
of device independence. They had no tool other than explicit pointers to functions, so they were
very, very careful.
19. The data structure was the stack frame of function calls. The language they created was Simula 67.
20. The history of the invention of Simula is fascinating. It is briefly described in the 1972 book
Structured Programming by Edsger W. Dijkstra, Ole-Johan Dahl, and C. A. R. Hoare (Academic
Press), and in much more detail in the paper “The Development of the Simula Languages” by
Dahl and Nygaard (https://hannemyr.com/cache/knojd_acm78.pdf).
153
Chapter 12 SOLID
I
I
HL1
+F()
ML1
+F()
HL1has a runtime dependency on F() within ML1; but HL1 has no source
code dependency, either direct or transitive, upon ML1. Instead, they both
depend upon the interface I.21
This ability to take any source code dependency and invert it provides us
with an immense amount of power. We can easily and safely arrange the
source code dependencies of our software to ensure that high-level
modules do not depend upon low-level modules.
Business
UI Database
Rules
Here we see the high-level business rules have runtime dependencies upon
the user interface (UI) and the database but have no source code
dependencies on those modules. This application of the DIP means that
the UI and database are plug-ins to the business rules and could easily be
replaced with different implementations without affecting the business
rules, thereby conforming to the OCP.
21. In dynamically typed languages, the interface I would not exist as a source code module. Rather,
it would be a duck type that HL1 and ML1 would conform to.
154
The Dependency Inversion Principle (DIP)
Of course, what’s really going on is that the UI and the database are
implementing interfaces contained within the business rules. The business
rules operate upon those interfaces, allowing the flow of control to go
outward toward the UI and database while keeping the source code
dependencies inverted inward toward the business rules (see Figure 12.5).
Business Rules
I I
UI DB
Figure 12.5. The interfaces within the business rules allow plug-ins.
Notice that all the dependencies point toward abstractions. This leads us
to one way to describe the DIP:
A B l a st from th e Pa st
But enough theory. Let’s see this at work. I’m going to borrow a nostalgic
example from my friend and mentor, Martin Fowler. He presented this
Video Store22 example in the first edition of his wonderful book,
Refactoring.23 Of course, I’m going to use Clojure instead of Java.
22. Video killed the radio store and the Internet killed the video store. Yes, boys and girls, there was a
time when we would go to the video store to rent videotapes and DVDs.
23. Addison-Wesley, 1999.
155
Chapter 12 SOLID
156
The Dependency Inversion Principle (DIP)
"\t8 1/2\t2.0\n"
"\tEraserhead\t3.5\n"
"You owed 7.5\n"
"You earned 3 frequent renter points\n")
(make-statement
(make-rental-order
@customer
[(make-rental
(make-movie "Plan 9 from Outer Space" :regular)
1)
(make-rental
(make-movie "8 1/2", :regular)
2)
(make-rental
(make-movie "Eraserhead" :regular)
3)])))))
From these tests, you should be able to determine what this application
does. Customers rent videos for a certain number of days. The price and
the reward points are apparently calculated based upon the type of the
video and the number of days they are rented. There seem to be three
types of videos: :regular, :new-release, and :childrens.
157
Chapter 12 SOLID
:new-release
(* 3.0 days)
:childrens
(if (> days 3)
(+ 1.5 (* (- days 3) 1.5))
1.5))))
158
The Dependency Inversion Principle (DIP)
If you read the first edition of Refactoring, this should look pretty
familiar. In essence, we have a simple report generator that calculates
and formats a statement for a rental order.
The very first thing you should have noticed is the horrific SRP violation
in the tests. Those tests couple the business rules with the construction
and formatting of the statement. If someone from marketing decides to
make even a trivial change to the statement format, all the tests will fail.
Consider, for example, the effects of changing the statement to begin with
the words “Rental Statement for” instead of “Rental Record for.”
This SRP violation makes the tests very fragile. To fix this we need to
separate the tests that specify the format of the report from the tests that
specify the business rules.
To do this I’m going to split the tests into three different modules: one
for testing the calculations, another for the formatting, and the last for
integration.
Here is the statement-calculator test. From now on, I’ll include all the
ns24 statements so that you can see the names of the modules and their
source code dependencies.
(ns video-store.statement-calculator-spec
(:require [speclj.core :refer :all]
[video-store.statement-calculator :refer :all]))
24. ns stands for namespace. These statements generally appear at the start of every Clojure module
and define the module’s name and its dependencies.
159
Chapter 12 SOLID
(declare customer)
160
The Dependency Inversion Principle (DIP)
:points 1}
(make-statement-data
(make-rental-order
@customer
[(make-rental
(make-movie "The Tigger Movie" :childrens)
3)]))))
What we’ve done here is replace the formatted rental statement with a
data structure that contains all the data that goes into the statement. This
allows us to separate the formatting from the calculation, as shown in the
statement-calculator implementation:
(ns video-store.statement-calculator)
161
Chapter 12 SOLID
:new-release
(* 3.0 days)
:childrens
(if (> days 3)
(+ 1.5 (* (- days 3) 1.5))
1.5))))
162
The Dependency Inversion Principle (DIP)
This is a bit simpler than before and is nicely encapsulated. Notice the ns
statement shows that this module has no source code dependencies.
Everything in the module is about the calculation of the data that goes
into the statement. However, there is nothing here that hints at the
formatting of the statement.
(ns video-store.statement-formatter-spec
(:require [speclj.core :refer :all]
[video-store.statement-formatter :refer :all]))
163
Chapter 12 SOLID
(ns video-store.statement-formatter)
To make sure that both of these modules work together as they should, I
added a simple integration test:
(ns video-store.integration-specs
(:require [speclj.core :refer :all]
[video-store.statement-formatter :refer :all]
[video-store.statement-calculator :refer :all]))
164
The Dependency Inversion Principle (DIP)
This is much better from an SRP point of view. If the marketing folks
make trivial changes to the format of the report, only the formatting and
integration tests will break. None of the calculation tests will break. That
might not seem like a big win in a toy example like this. But in a real-
world application where the tests would number in the thousands, this is
a very big win indeed.
We are also protected from business rule changes. If the finance people
decide they need to change the way prices are calculated, the formatting
test will be immune, and only the calculation and integration tests will be
affected.
A D I P Viol ation
While all this winning was going on, did you happen to notice the DIP
violation? You might have missed it because it’s not in the production
code. It’s in the integration test.
165
Chapter 12 SOLID
Look at the ns statement. Do you see those two lines that mention the
statement-formatter and the statement-calculator? Those lines create
source code dependencies on the concrete implementations of those
modules. That’s a high-level policy depending on a concrete low-level
detail. That’s a definitional DIP violation.
Perhaps this puzzles you. How can a test be a high-level policy? Aren’t
tests as low level as you can get? Aren’t they the ultimate details?
Yes, that’s true. But integration tests in particular are stand-ins for high-
level policy. Look at that integration test again. It does precisely what
the high-level policy of the application would have to do. It calls make-
statement-data and passes the result to format-rental-statement. And
since both of those functions are concrete implementations, our high-level
production code will have the same DIP violation as our integration test.
But perhaps you are still not convinced. So let’s add a new feature.
Sometimes we want the statement to be displayed on a text terminal,
and sometimes we want it on a browser. So we need text and HTML
versions of format-rental-statement.
Let’s also add one more new feature. Some of our stores are offering a
“buy two, get one free” policy. So, if you rent three videos, you will only
be charged for the two most expensive ones.
25. I spend a lot of time on this topic in my book Clean Craftsmanship (Addison-Wesley, 2021).
166
The Dependency Inversion Principle (DIP)
We can easily mimic this design by using any one of the three approaches
that we discussed in the section on the OCP. We could build vtables for
the two abstractions. Or we could use defprotocol and defrecord to
build actual Java interfaces and implementations. Or, finally, we could use
multi-methods.
Let’s see what the multi-method approach looks like. Keep in mind that
this is a child-sized problem posing as an adult situation. What you’ll see
me do here is meant to show how much larger problems can be designed
and partitioned.
In the end, as shown in Figure 12.6, I split the whole system up into
eleven modules, three of which are tests.
Figure 12.6 looks like a UML diagram for an OO solution. The dependency
inversion should be obvious. The order-processing module is the highest-
level policy. It depends upon two abstractions. The statement-formatter is
an interface, whereas the statement-policy is an abstract class with one
implemented method.
167
Chapter 12 SOLID
Order-Processing
+ process-order
I A
Statement-Formatter Statement-Policy
+ format-rental-statement + make-statement-data
– determine-amount {A}
– determine-points {A}
– total-amount {A}
– total-points {A}
Text-Formatter HTML-Formatter
Normal Statement
Policy
T
Integration
Spec Buy Two
Get One Free Policy
T T
Statement Formatter Statement Policy
Constructors
Spec Spec
The tests appear at the bottom. They are marked with <T>. They use a
little utility module named constructors that knows how to build the
basic data structures. Then each uses its particular portion of the
production code to test what it needs.
Now let’s look at the source code. Pay special attention to the ns statements
and notice that they match the arrows on the UML diagram.
168
The Dependency Inversion Principle (DIP)
(ns video-store.constructors)
(ns video-store.integration-specs
(:require [speclj.core :refer :all]
[video-store.constructors :refer :all]
[video-store.text-statement-formatter :refer :all]
[video-store.normal-statement-policy :refer :all]
[video-store.order-processing :refer :all]))
(declare rental-order)
169
Chapter 12 SOLID
1)
(make-rental
(make-movie "8 1/2", :regular)
2)
(make-rental
(make-movie "Eraserhead" :regular)
3)]))
(it "formats a text statement"
(should= (str "Rental Record for Fred\n"
"\tPlan 9 from Outer Space\t2.0\n"
"\t8 1/2\t2.0\n"
"\tEraserhead\t3.5\n"
"You owed 7.5\n"
"You earned 3 frequent renter points\n")
(process-order
(make-normal-policy)
(make-text-formatter)
@rental-order))))
This is pretty much the same as before, except that the ns statement has
all the explicit source code dependencies. This test still violates the DIP,
but only because it must call the make-normal-policy and make-text-
formatter constructors within the corresponding modules. I suppose I
could have used an Abstract Factory26 to break those last dependencies;
but it didn’t seem worth the effort for a test that tests integration.
The other two tests are more specific. Pay special attention to the fact
that their source code dependencies only pull in what they need:
(ns video-store.statement-formatter-spec
(:require [speclj.core :refer :all]
[video-store.statement-formatter :refer :all]
[video-store.text-statement-formatter :refer :all]
[video-store.html-statement-formatter :refer :all]))
(declare statement-data)
170
The Dependency Inversion Principle (DIP)
(ns video-store.statement-policy-spec
(:require
[speclj.core :refer :all]
171
Chapter 12 SOLID
172
The Dependency Inversion Principle (DIP)
(make-rental-order
@customer
[(make-rental @new-release-1 3)
(make-rental @new-release-2 3)]))))
173
Chapter 12 SOLID
(make-statement-data
(make-buy-two-get-one-free-policy)
(make-rental-order
@customer
[(make-rental @regular-1 1)
(make-rental @regular-2 1)
(make-rental @new-release-1 1)]))))))
(ns video-store.order-processing
(:require [video-store.statement-formatter :refer :all]
[video-store.statement-policy :refer :all]))
There’s not much to it. Notice the source code dependencies only refer to
the statement-formatter interface and the statement-policy abstraction.
(ns video-store.statement-formatter)
(defmulti format-rental-statement
(fn [formatter statement-data]
(:type formatter)))
174
The Dependency Inversion Principle (DIP)
(ns video-store.statement-policy)
(ns video-store.text-statement-formatter
(:require [video-store.statement-formatter :refer :all]))
175
Chapter 12 SOLID
(defmethod format-rental-statement
::text
[_formatter statement-data]
(let [customer-name (:customer-name statement-data)
movies (:movies statement-data)
owed (:owed statement-data)
points (:points statement-data)]
(str
(format "Rental Record for %s\n" customer-name)
(apply str
(for [movie movies]
(format "\t%s\t%.1f\n"
(:title movie)
(:price movie))))
(format "You owed %.1f\n" owed)
(format "You earned %d frequent renter points\n" points))))
This shouldn’t be much of a surprise. I just moved the code over here
without much change. Notice the make-text-formatter function at the top.
(ns video-store.html-statement-formatter
(:require [video-store.statement-formatter :refer :all]))
176
The Dependency Inversion Principle (DIP)
(apply str
(for [movie movies]
(format "<tr><td>%s</td><td>%.1f</td></tr>"
(:title movie) (:price movie))))
"</table>"
(format "You owed %.1f<br>" owed)
(format "You earned <b>%d</b> frequent renter points"
points))))
The more interesting modules are the two policy modules. Let’s begin
with normal-statement-policy:
(ns video-store.normal-statement-policy
(:require [video-store.statement-policy :refer :all]))
(defmethod determine-amount
[::normal :childrens]
[_policy rental]
(let [days (:days rental)]
(if (> days 3)
(+ 1.5 (* (- days 3) 1.5))
1.5)))
(defmethod determine-amount
[::normal :new-release]
[_policy rental]
(* 3.0 (:days rental)))
177
Chapter 12 SOLID
(defmethod determine-points
[::normal :new-release]
[_policy rental]
(if (> (:days rental) 1) 2 1))
(defmethod determine-points
[::normal :childrens]
[_policy _rental]
1)
You might be worried that the two degrees of freedom will create an
N*M problem, leading to a proliferation of the “determine” functions.
You’ll see how I handle that in a minute.
(ns video-store.buy-two-get-one-free-policy
(:require [video-store.statement-policy :refer :all]
[video-store.normal-statement-policy :as normal]))
(defn make-buy-two-get-one-free-policy []
{:type ::buy-two-get-one-free})
178
The Dependency Inversion Principle (DIP)
(defmethod total-amount
::buy-two-get-one-free
[policy rentals]
(let [amounts (map #(determine-amount policy %) rentals)]
(if (> (count amounts) 2)
(reduce + (drop 1 (sort amounts)))
(reduce + amounts))))
What this says to the compiler is that it should use the :normal
implementations unless overridden by a specific ::buy-two-get-one-
free implementation.
Thus, our module only has to override the total-amount function in order
to subtract the least expensive movie if three or more are rented.
Conc lusion
OK, that’s it. We’ve chopped this system up into 11 modules. Each
module is nicely encapsulated. We have inverted the most important
source code dependencies so that high-level policies do not depend upon
low-level details.
Nice.
29. Once again, don’t worry about the double colons. They are just a way to scope keywords into a
namespace.
179
This page intentionally left blank
IV
Functional
Pr agmatics
181
This page intentionally left blank
13
Tests
183
Chapter 13 Tests
Throughout this book, you’ve seen many of the unit tests I have written.
In virtually every case, I used the TDD1 discipline of writing my tests and
code in a tight loop, with the tests a few seconds ahead of the code.
For the most part, those tests were written using a framework called
speclj2 (pronounced “speckle”), written by Micah Martin and others. It
is very similar to the RSpec framework that is popular in Ruby.
I have been practicing TDD for well over 20 years now. I’ve used it in Java,
C#, C, C++, Ruby, Python, Lua, Clojure, and a variety of other languages.
What I have learned in those decades is that the language does not matter
to the discipline. The discipline is the same regardless of the language.
The fact that Clojure is a functional language does not change my testing
strategy, nor affect my use of the TDD discipline. I write my Clojure
programs test-first the way I write my Java programs test-first. The
paradigm doesn’t matter. The discipline is universal.
Wh at a bout M oc k s ?
Mocking is a technique used by TDD practitioners to encapsulate their
tests away from large swaths of the system. In effect, they create objects,
1. I have written a great deal about this discipline in Clean Craftsmanship (Addison-Wesley, 2021),
Clean Code (Pearson, 2008), and Agile Software Development: Principles, Patterns, and Practices
(Pearson, 2002). There is also a vast amount of information available on the Web. One of the best
books on the topic is Growing Object-Oriented Software, Guided by Tests by Steve Freeman and
Nat Pryce (Addison-Wesley, 2010).
2. https://github.com/slagyr/speclj
184
What about Mocks?
called mocks,3 that represent those swaths and use the LSP to substitute
the mocks in for them.
But as we have seen, the LSP works just as well in a functional language
as it does in an OO language, and polymorphic interfaces are generally
very easy to create. Thus, the ability to write mocks, in all their various
forms, is not at all impeded in a functional language.
Don’t worry too much about what this test does. Just look down at the
with-redefs statement. This test mocks the swing-util/add-id-to-tab
and swing-util/relaunch functions to use named stubs. Those stubs are
perfect no-ops. They accept any number of arguments and return nothing
3. They are more formally referred to as test-doubles, but in this context, I’ll continue to use the col-
loquial vernacular.
4. https://github.com/unclebob/more-speech
185
Chapter 13 Tests
at all.5 But they do remember what happened to them.6 So, down at the
bottom, we see that the :relaunch stub should have been called, and the
:add-id-to-tab stub should have been called with three arguments: "tab",
:selected, and 1.
Prope rt y- Ba s e d Te sting
One cannot hang out with functional programmers without eventually
hearing about QuickCheck and property-based testing. Unfortunately, the
topic often arises as a counterargument to TDD. I’m not going to try to
support or refute that argument. Instead, I want to show you how very
powerful property-based testing is within the TDD discipline.
Let’s say that I’ve just written a function that computes the prime factors
of a given integer:
5. There are ways to get them to return values, but that’s beyond the scope here. Check the speclj
docs (https://github.com/slagyr/speclj) if you are interested.
6. Which technically makes them spies.
186
Property-Based Testing
Let’s also say that I wrote this function using TDD. Here are my tests:
Pretty cool, right? But how certain am I that this function actually works?
I mean, how do I know that there isn’t some horrible corner case where
the function fails unexpectedly?
187
Chapter 13 Tests
Of course, I may never be perfectly sure about this; but there are some
things I can do to make myself a lot more comfortable. One property of
the output is that the product of all the factors will equal the input. So
why don’t I generate a thousand random integers and make sure that the
prime factors of each multiply together to equal them.
(declare n)7
(describe "properties"
(it "multiplies out properly"
(should-be
:result
(tc/quick-check
1000
(prop/for-all
[n gen-inputs]
(let [factors (factors-of n)]
(= n (reduce * factors))))))))
The test tells QuickCheck to run 1,000 times. For each integer, it calculates
the prime factors, multiplies them all together, and makes sure that the
product equals the input. Nice.
The tc/quick-check function returns a map with the results. The :result
element of that map will be true if all the checks passed; and that’s what
the should-be :result asserts.
7. A forward declaration of n.
8. https://clojure.org/guides/test_check_beginner
188
Property-Based Testing
There is another property of the prime factors: They should all be prime.
So let’s write a function that tests for primality:
(describe "factors"
(it "they are all prime"
(should-be
:result
(tc/quick-check
1000
(prop/for-all
[n gen-inputs]
(let [factors (factors-of n)]
(every? is-prime? factors)))))))
OK. So now we know that this function returns a list of integers, each of
which is prime, and that when multiplied together equal the input. That’s
kind of the definition of prime factors.
So this is nice. I can randomly generate a bunch of inputs and then apply
property checks to the outputs.
189
Chapter 13 Tests
Remember our Video Store example from the preceding chapter? Let’s do
some property-based testing on that.
190
A Diagnostic Technique
(def gen-customer-name
(gen/such-that not-empty gen/string-alphanumeric))
(def gen-customer
(gen/fmap (fn [name] {:name name}) gen-customer-name))
(def gen-movie-type
(gen/elements [:regular :childrens :new-release]))
(def gen-movie
(gen/fmap (fn [[title type]] {:title title :type type})
(gen/tuple gen/string-alphanumeric gen-movie-type)))
(def gen-rental
(gen/fmap (fn [[movie days]] {:movie movie :days days})
(gen/tuple gen-movie gen-days)))
(def gen-rentals
(gen/such-that not-empty (gen/vector gen-rental)))
(def gen-rental-order
(gen/fmap (fn [[customer rentals]]
{:customer customer :rentals rentals})
(gen/tuple gen-customer gen-rentals)))
191
Chapter 13 Tests
I’m not going to explain the ins and outs of clojure.check here, but I will
walk through what the generators do.
Do you notice an eerie similarity between the type specification and the
generator? So do I. Here are a few sample outputs from the generator:
[
{:customer {:name "5Q"},
:rentals [{:movie {:title "", :type :new-release}, :days 52}]}
192
A Diagnostic Technique
193
Chapter 13 Tests
Just a bunch of random data that conforms nicely to the type of a rental-
order. But let’s check that:
194
A Diagnostic Technique
So here we see the clojure.spec for the statement-data, and the quick-
check that makes sure that the output of make-statement-data conforms
to it. Nice.
With all this passing, we can be pretty sure that the generator is generating
valid rental orders. So now let’s get on with the property checks.
195
Chapter 13 Tests
policy gen-policy]
(let [statement-data (make-statement-data
policy rental-order)
prices (map :price (:movies statement-data))
owed (:owed statement-data)]
(= owed (reduce + prices)))))))
{:shrunk
{:total-nodes-visited 45,
:depth 14,
:pass? false,
:result false,
:result-data nil,
:time-shrinking-ms 3,
:smallest
[{:customer {:name "0"},
:rentals [{:movie {:title "", :type :regular}, :days 1}
{:movie {:title "", :type :regular}, :days 1}
{:movie {:title "", :type :regular}, :days 1}]}
{:type
:video-store.
buy-two-get-one-free-policy/buy-two-get-one-free}]},
:failed-after-ms 0,
:num-tests 7,
:seed 1672092997135,
:fail
[{:customer {:name "4s7u"},
:rentals
[{:movie {:title "i7jiVAd", :type :childrens}, :days 85}
{:movie {:title "7MQM", :type :new-release}, :days 26}
{:movie {:title "qlS4S", :type :new-release}, :days 99}
{:movie {:title "X", :type :regular}, :days 87}
{:movie {:title "w1cRbM", :type :regular}, :days 11}
{:movie {:title "7Hb41O5", :type :regular}, :days 63}
{:movie {:title "xWc", :type :childrens}, :days 41}]}
196
Functional
{:type
:video-store.
buy-two-get-one-free-policy/buy-two-get-one-free}],
:result false,
:result-data nil,
:failing-size 6,
:pass? false}
Yes, I know this looks awful; but this is where the real magic of quick-
check shines through, so bear with me.
First of all, do you see that top element named :shrunk? That’s a big clue
to what is going on here. When quick-check finds an error, it begins
hunting for the smallest randomly generated input that continues to
produce that error.
So look at the :fail element. That’s the rental-order that caused the
initial failure. Now look at the :smallest element within the :shrunk
element. The quick-check function managed to shrink the rental-order
down while preserving the failure. That’s the smallest rental-order that it
could find that failed.
And why did it fail? Notice that there are three movies. Notice also that
the policy is buy-two-get-one-free. Ah, of course, under that policy the
sum of the movies is not equal to the :owed element.
Functional
So why are tools like quick-check not more popular in OO languages?
Perhaps it’s because they work best with pure functions. I imagine it’s
possible to set up generators and test properties in a mutable system, but
it’s likely a lot more complicated than in an immutable system.
197
This page intentionally left blank
14
GU I
199
Chapter 14 GUI
Over the years, I have used two different GUI frameworks in functional
programs. The first is named Quil,1 and it is based upon the popular Java
framework named Processing.2 The second is SeeSaw,3 which is based
upon the old Java Swing4 framework.
One of the first programs I wrote using Quil was spacewar. I’ve
mentioned it a few times in this book. If you’d like to see the program
in action, you can go to https://github.com/unclebob/spacewar where
there is a ClojureScript version you can run in your browser. I did not
write spacewar to be used in ClojureScript; but Mike Fikes ported it
over in a day or so. It actually works better in my browser than it does
in native Clojure on my laptop.
Turtle graphics6 are a simple set of commands that were invented for the
Logo language in the late 1960s. Those commands controlled a robot
called a turtle. The robot sat on a large piece of paper and had a pen that
1. www.quil.info
2. https://processing.org
3. https://github.com/clj-commons/seesaw
4. https://en.wikipedia.org/wiki/Swing_(Java)
5. https://github.com/unclebob/turtle-graphics
6. https://en.wikipedia.org/wiki/Turtle_graphics
200
Turtle-Graphics in Quil
could be raised and lowered onto the paper. The robot could be told to
move forward or backward a certain distance, or to turn a number of
degrees left or right.
Figure 14.1 is a picture of the inventor, Seymour Papert, with one of his
turtles.
So, for example, if you’d like to draw a square, you might issue these
commands:
Pen down
Forward 10
Right 90
Forward 10
Right 90
Forward 10
Right 90
Forward 10
Pen up.
201
Chapter 14 GUI
My goal was not to create a turtle graphics console on which you would
type commands. Instead, I wanted a turtle graphics API that I could use
to write graphical functions in Clojure.
(defn turtle-script []
(polygon 144 400 5))
That program draws the picture in Figure 14.2. (Notice the little turtle
sitting on the left vertex of the star.)
202
Turtle-Graphics in Quil
Perhaps you’ve noticed that the polygon function does not appear to
be functional because it doesn’t produce a return value from its inputs.
Instead, it has the side effect of drawing on the screen. Moreover, each
of the commands mutates the state of the turtle. So turtle-graphics
programs are not functional.
203
Chapter 14 GUI
I’m not going to do a full tutorial on Quil here, but there are a few things
I should point out. Take note of the :setup, :update, and :draw elements.
Each points to a function.
The setup function will be called once at the start of the program.
204
Turtle-Graphics in Quil
If you think of this as a tail recursive loop, then the contents of the screen
are the tail recursive values. So even though we are mutating the contents
of the screen, we are doing so at the tail of the recursion where the
mutation is harmless.9 So, although not purely functional, it is as
“functional” as any TCO10 system can be.
(defn setup []
(q/frame-rate 60)
(q/color-mode :rgb)
(let [state {:turtle (turtle/make)
:channel channel}]
(async/go
(turtle-script)
(prn "Turtle script complete"))
state))
This starts out pretty simple. It sets the frame rate to 60fps and the color
mode to RGB, and it creates the state object that will be passed to
update-state and draw-state.
The state object is composed of a channel and the turtle. We’ll talk
about the channel later. For the moment, let’s concentrate on the turtle:
9. Mostly harmless.
10. Remember our discussion about tail call optimization back in Chapter 1.
205
Chapter 14 GUI
(defn make []
{:post [(s/assert ::turtle %)]}
{:position [0.0 0.0]
:heading 0.0
:velocity 0.0
:distance 0.0
:omega 0.0
:angle 0.0
:pen :up
:weight 1
:speed 5
:visible true
:lines []
:state :idle})
206
Turtle-Graphics in Quil
This shows the type specification of the turtle, followed by its constructor.
Notice that the constructor checks the type as a :post condition. The
elements of the turtle are mostly self-explanatory. There’s the XY position,
the angular heading, the velocity, the up/down state of the pen, the drawing
weight of the pen, the visibility state, and so on. The other elements will
come to light soon enough.
——Turtle module——
207
Chapter 14 GUI
(q/with-rotation
[heading]
(q/line 0 base-left 0 base-right)
(q/line 0 base-left tip 0)
(q/line 0 base-right tip 0))))))
The draw-state function, which is called by Quil 60 times each second, sets
the background color of the screen to light gray, centers the drawing at
(500, 500), and then calls turtle/draw, which draws the current line in
progress and then all the other lines that were previously drawn. Finally, it
draws the turtle itself. Notice how Quil helps with translation and rotation.
(defn update-position
[{:keys [position velocity heading distance] :as turtle}]
(let [step (min (q/abs velocity) distance)
distance (- distance step)
step (if (neg? velocity) (- step) step)
radians (q/radians heading)
[x y] position
vx (* step (Math/cos radians))
vy (* step (Math/sin radians))
position [(+ x vx) (+ y vy)]]
(assoc turtle :position position
:distance distance
:velocity (if (zero? distance) 0.0 velocity))))
208
Turtle-Graphics in Quil
209
Chapter 14 GUI
Notice that update-turtle has a :post condition that checks the type of
the turtle after it has been updated. It’s nice to know that when you
update a big structure you haven’t messed up some little part of it.
If we are done and the pen is down, then we add the line in progress to
the list of previous lines, and we update the pen-start to the current
position to prepare for the next line.
Updating the position and heading are simple functions that do the
necessary trig calculations to place the turtle in the proper position
and orientation. They both use the turtle’s :velocity to adjust how
big a step they take at each update.
If the turtle is :idle, then we are ready for a command. So we poll the
channel. If there is a command on the channel, we process it by calling
turtle/handle-command, and then repeat until no commands are left on
the channel.
210
Turtle-Graphics in Quil
211
Chapter 14 GUI
We simply translate the command tokens into function calls. Not really
rocket science. The command functions manage the state of the turtle.
Take for instance, the forward command. It sets the turtle’s :state to
:busy, sets the turtle’s :velocity, and sets the :distance it must move
before going :idle again.
OK, we’re almost done. Now all we need to do is look at the way the
turtle-script function sends commands to the channel:
212
Turtle-Graphics in Quil
The async/>!! function sends its argument to the channel. If the channel
is full, it waits. That really wasn’t very surprising, was it?
And with that, we can put all the turtle graphics commands we like into
the turtle-script function and watch the turtle dance around the screen
drawing our pretty pictures.
213
This page intentionally left blank
15
Concurrency
215
Chapter 15 Concurrency
Or can they?
While comforting, those “facts” are not precisely true. The purpose of this
chapter is to show how multithreaded “functional” programs can still
have race conditions.
To examine this, let’s set up some interacting finite state machines. One of
my favorite examples is the making of a telephone call in the 1960s. The
sequence of events looked roughly like Figure 15.1.
ck
Ringba Ring
ok
Off Ho
C onnect Conne
ct
Hello
216
Concurrency
This is a message sequence chart. Time is on the vertical axis, and all
messages are angled because they all take time to send.
Bob wants to place a call to Alice. Bob lifts the telephone receiver off its
hook1 and holds it to his ear. The telephone company (telco) sends a dial
tone2 to the receiver. Upon hearing that tone, Bob dials3 Alice’s number.
The telco then sends a ringing voltage4 to Alice’s phone and a ringback5
tone to Bob’s receiver. Alice hears the ringing of her phone and lifts the
receiver off the hook. The telco connects Bob to Alice, and Alice says
“Hello” to Bob.
There are three finite state machines running in this scenario: Bob, telco,
and Alice. Bob and Alice run separate instances of the User state machine6
shown in Figure 15.2.
In these diagrams, the -> symbol means to send the corresponding event
to the other state machine.
1. Telephones in the early 20th century had a hook that the receiver hung on. By the 1960s, the hook
had been replaced by a cradle that the receiver sat in; but it was still called the hook.
2. This was a very recognizable sound that meant that the telephone system was ready for you to dial
the number you wanted to call.
3. The verb dial means to enter the telephone number. In the early 1960s, this was accomplished by
using a rotary dial on the face of the telephone.
4. 90 volts in the United States.
5. Another very distinct sound that was meant to entertain the caller while waiting for the called
phone to be answered.
6. These state machines are abbreviated to keep them simple. In reality, all the states would have
transitions back to Idle.
217
Chapter 15 Concurrency
Dial Tone
Calling
Dial
Call Dialing
Caller
Off Hook Ringback
Idle
Waiting
for
connection
Ring
Connected
Caller . . . Hang Up
Off Hook
Talking
Disconnected
Waiting Dial
Caller for Dial Ring
Off Hook Ringback Waiting
Dial Tone for
Answer Caller
Idle Off Hook
Connect
Waiting
for
Hang up
Hang up
Disconnect
So when Bob decides to make a call (the call event from the Idle state) the
User state machine sends the off-hook event to the Telco. When the Telco
is in the Waiting for Dial state and receives the Dial event from the User,
it sends the Ring and Ringback events to the appropriate User state
machines.
218
Concurrency
If you study these diagrams carefully, you should be able to see how the
state machines and messages interact to allow Bob to call Alice.
(def user-sm
{:idle {:call [:calling caller-off-hook]
:ring [:waiting-for-connection callee-off-hook]
:disconnect [:idle nil]}
:calling {:dialtone [:dialing dial]}
:dialing {:ringback [:waiting-for-connection nil]}
:waiting-for-connection {:connected [:talking talk]}
:talking {:disconnect [:idle nil]}})
(def telco-sm
{:idle {:caller-off-hook [:waiting-for-dial dialtone]
:hangup [:idle nil]}
:waiting-for-dial {:dial [:waiting-for-answer ring]}
:waiting-for-answer {:callee-off-hook
[:waiting-for-hangup connect]}
:waiting-for-hangup {:hangup [:idle disconnect]}})
Each state machine is simply a hash map of states, each of which contains
a hash map of events that specify the new state and the action to be
performed.
So when the user-sm is in the :idle state and it gets a :call event, it
transitions to the :calling state and calls the caller-off-hook function.
219
Chapter 15 Concurrency
An agent is initialized with a data structure and then serializes all updates
to that data structure, thereby eliminating all concurrent update issues.
Here are the functions that create the two different agents:
7. The get-in function returns an element from a nested map. (get-in {:a {:b 2}} [:a :b])
returns 2.
220
Concurrency
transition function are the event (:call) and the data that should be
passed to the action function. In this case, the data is a list of the three
agents that represent the finite state machines in the system.
(defn caller-off-hook
[sm-agent [telco caller callee :as call-data]]
(swap! log conj (str (:name @caller) " goes off hook."))
(send telco transition :caller-off-hook call-data))
(defn callee-off-hook
[sm-agent [telco caller callee :as call-data]]
(swap! log conj (str (:name @callee) " goes off hook"))
(send telco transition :callee-off-hook call-data))
221
Chapter 15 Concurrency
This test passes, which means that all the state machines returned to the
idle state by the time 100ms had passed. The log output looks like this:
8. In short, destructuring is a convenient way of breaking a complex data element into named com-
ponents. See the Clojure documentation for more details.
222
Concurrency
You can see how the threads interleaved with one another, while all three
finite state machines worked together to drive the call to a successful
completion.
The three agents have mutable state; but there can be no concurrent
update problems because the agents serialize their operations. So no race
conditions, right?
What I’m about to show you in Figure 15.4, is a race condition that
existed in the telephone system in the ‘60s.9 Once again, we begin with
Bob calling Alice. But this time Alice is just about to call Bob.
Bob Telco Alice
Off Ho
ok
ne
Dial To
Dial
ok
Ho
ck Ring Off
Ringba
t
Connec Conne
ct
Silence
223
Chapter 15 Concurrency
Do you see what went wrong? Those crossed lines are the problem. That’s
a race condition. The telco tried to ring Alice’s phone; but before it could
make the sound, Alice picked up the receiver in order to call Bob. From
the point of view of the telco, everything is fine. It rang the phone and
Alice picked up. So the telco happily connects Bob and Alice. But Alice is
sitting there waiting for a dial tone; and Bob is confused because nobody
has said hello and the ringback tone has stopped.
The most likely outcome is that both parties hang up without talking
to each other. Alternatively, Alice might say something and Bob might
respond, and they’d get into the comic routine of who called who.
Can we make our state machines emulate this fault? Here’s the setup,
once again posed as a test:
Notice that we now have four state machines: one for Bob, one for Alice,
and one telco for each of the two calls. The test fails. After 100ms, the
state machines have not returned to the Idle state.
224
Conclusion
This took me several tries, because the window for that particular race
condition is pretty narrow. But there it is. See that TILT!? That’s what our
transition function puts in the log if it is ever asked to make an invalid
transition. Alice is still in the :calling state waiting for the :dialtone
event, and has no way to deal with the :ring event.
The bottom line is that race conditions are still possible even though
concurrent updates are not. That’s because it is always possible to construct
interacting state machines that get out of sync with one another.
Conc lusion
Somewhere around the turn of the century, Moore’s law died. Clock rates
hit a maximum of about 3GHz and then just stopped increasing. To drive
more throughput, hardware engineers started putting more processors
on their chips. We went through the dual-core stage and the quad-core
stage—and we thought we were going to see a doubling in cores every
other year or so. We started to fret about the possibility of dealing with
machines that had 32, or 64, or 128 cores.
But Moore’s law wasn’t done dying. It died for clock speed a few years
before it died for component density. So, for the past decade or more, our
225
Chapter 15 Concurrency
And that’s probably a good thing because, as this chapter has shown, the
reasoning was somewhat faulty to begin with. Race conditions might be
more common in threads that have mutable variables, but in any system
where there are concurrent finite state machines, the possibility exists that
race conditions might drive them out of sync with one another.
226
D esign Pat terns
V
The idea of design patterns1 was one of the most profound in the software
industry. It ranks up there with structured programming, object-oriented
programming, and functional programming. It told us that applications
consist, in part, of repeatable and reusable elements. Those elements solved
problems common to many, if not all, applications.
Of course, like all good ideas in software, design patterns have been
misunderstood, overused, abused, and even discarded as archaic or
specific to only very narrow contexts. This is a shame, because design
patterns are eminently useful.
1. The definitive work on this topic was Erich Gamma, Richard Helm, Ralph Johnson, and
John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-
Wesley, 1994).
227
This page intentionally left blank
16
D esign Pat terns
R eview
229
Chapter 16 Design Patterns Review
Long ago, in a decade far, far away, I was a prolific writer on a social
network called comp.object.2 In this group, we debated issues of OO
design.
One day someone posed a simple problem and suggested that we all solve
it in our own way and then debate the result. The problem was:
Given a switch and a light, make the switch turn the light on.
Light
Switch
+ Turn on
Figure 16.1. The simplest solution for the switch and the light
The Switch class3 calls the TurnOn method of the Light class.
The objection to this was that the Switch class could be used to turn on
other things like Fans or Televisions. Therefore, the Switch class should
not know about the Light class. An abstraction should be imposed
between the two, as shown in Figure 16.2.
Now the Switch class uses an interface named Switchable. The Light
class implements Switchable.
This solves the problem. Now we could have any number of devices
controlled by the Switch. This solution is one of the simplest expressions
2. A newsgroup within the vast array of newsgroups transmitted by Network News Transport
Protocol (NNTP) over Unix-to-Unix copy (UUCP) and the Internet.
3. Remember, this was an OO forum. Don’t get hung up on the word class.
230
Design Patterns Review
of the DIP, the OCP, and the LSP. It also has a name. It’s called Abstract
Server.4
I
Switch Switchable
+ Turn on
Light
But what about the context part of the design pattern? Well, let’s go back
to our team. Someone has just suggested using the Abstract Server
pattern. Another team member says, “No, you don’t understand, we don’t
own the Light class; it’s part of a third-party library, so we can’t alter it
to implement an interface.”
So, the context of the problem is that we want to decouple Switch from
Light, but we can’t modify Light. So someone else on the team says,
“Well, we could use an Adapter.”
4. Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (Pearson,
2002), 318.
231
Chapter 16 Design Patterns Review
If you were on the team and didn’t know what the Adapter pattern was,
you wouldn’t understand their suggestion. But if you were aware of the
design patterns canon, you could swiftly assess the suggestion. Again, the
benefit of design patterns is knowing the names and the canonical forms
so that you can quickly apply them.
Light Light
Adapter + Turn on
Just as they are about to move on to the next issue, someone on the team
says, “Wait, which form of the Adapter should we use?”
It turns out that the canonical name for a design pattern does not
necessarily describe a single solution. Some of the patterns have multiple
forms. The Adapter is one such pattern. It could look like Figure 16.3, or
it could look like Figure 16.4.
The former is called the object form of the Adapter because the
LightAdapter is its own object. The latter is the class form of the Adapter
because the LightAdapter is a subclass of Light.
The team members debate the two forms for a moment and come to the
decision that the class form of the Adapter is sufficient for the moment
and will relieve them of the complication of constructing a separate
LightAdapter object.
232
Patterns in Functional Programming
I
Switchable
Switch
+ Turn on
Light Light
Adapter + Turn on
As you’ll see in the pages that follow, there are indeed aspects of certain
design patterns that appear to be workarounds for certain inadequacies in
OO languages; but this is hardly applicable to all design patterns. Moreover,
even those particular design patterns have a more general form in which
they are applicable in functional languages.
A bstr act S e rv e r
So, what does the Abstract Server look like in a functional language?
233
Chapter 16 Design Patterns Review
(defn turn-on-light []
;turn on the bloody light!
)
(defn engage-switch []
;Some other stuff. . .
(turn-on-light))
OK, that’s not rocket science. However, the original problem is immediately
evident. Our engage-switch function has a direct dependency on turn-on-
light, which means we can’t use it to turn on a fan or a television or
anything else. So, what should we do?
That works in the simplest case. But let’s make the problem just a bit
more interesting. Let’s say that our engage-switch function must turn the
light both on and off at various times. Perhaps it’s part of some home
security system with special timers for the lights. This changes the original
problem to look like this:
(defn turn-on-light []
;turn on the bloody light!
)
(defn turn-off-light []
;Criminy! just turn it off!
)
234
Abstract Server
(defn engage-switch []
;Some other stuff...
(turn-on-light)
;Some more other stuff...
(turn-off-light))
(defn make-switchable-light []
{:on turn-on-light
:off turn-off-light})
Yeah, that’s actually pretty nice. And since Clojure is a dynamically typed
language, we don’t have the problem that an inheritance or implements
relationship would cause.
Of course, we could have solved this with the multi-method form of the
Abstract Server pattern:
235
Chapter 16 Design Patterns Review
(turn-on switchable)
;Some more other stuff...
(turn-off switchable))
(describe "switch/light"
(with-stubs)
(it "turns light on and off"
(with-redefs [turn-on-light (stub :turn-on-light)
turn-off-light (stub :turn-off-light)]
(engage-switch {:type :light})
(should-have-invoked :turn-on-light)
(should-have-invoked :turn-off-light))))
The two stubs mock out the target functions. We invoke the engage-
switch function with the {:type :light} argument. Then we test that
the two target functions were, in fact, called.
A da p te r
The Adapter pattern is used whenever you have a client who wants to use
a server, but the interface that the client expects and the interface that the
server expresses are incompatible.
236
Adapter
Perhaps the simplest form of the Adapter might look like this:
(describe "Adapter"
(with-stubs)
(it "turns light on and off"
(with-redefs [turn-on-light (stub :turn-on-light)]
(engage-switch {:type :variable-light})
(should-have-invoked :turn-on-light {:times 1 :with [100]})
(should-have-invoked :turn-on-light {:times 1 :with [0]}))))
237
Chapter 16 Design Patterns Review
If I were to draw this structure in the UML, I’d likely draw something
like Figure 16.5.
I
Engage Switchable
Switch + Turn on
+ Turn off
Variable
Variable
Light
Light
Adapter
Perhaps you don’t find this convincing. After all, it’s just a simple little
program with a couple of defmulti functions. There’s no obvious OO
structure like that shown in the UML. So let’s impose that structure by
splitting up the source files.
(ns turn-on-light.switchable)
238
Adapter
(ns turn-on-light.engage-switch
(:require [turn-on-light.switchable :as s]))
————————————————
(ns turn-on-light.variable-light)
(ns turn-on-light.variable-light-adapter
(:require [turn-on-light.switchable :as s]
[turn-on-light.variable-light :as v-l]))
239
Chapter 16 Design Patterns Review
(defn make-adapter []
{:type :variable-light})
And lastly, the test ties everything together in a nice, neat little ball by
depending upon all the concrete namespaces:
(ns turn-on-light.turn-on-spec
(:require [speclj.core :refer :all]
[turn-on-light.engage-switch :refer :all]
[turn-on-light.variable-light :as v-l]
[turn-on-light.variable-light-adapter
:as v-l-adapter]))
(describe "Adapter"
(with-stubs)
(it "turns light on and off"
(with-redefs [v-l/turn-on-light (stub :turn-on-light)]
(engage-switch (v-l-adapter/make-adapter))
(should-have-invoked :turn-on-light
{:times 1 :with [100]})
(should-have-invoked :turn-on-light
{:times 1 :with [0]}))))
Look through those source code dependencies and compare them to the
UML diagram, and you’ll see that they match perfectly.
So which form of the Adapter pattern was this? We might call it the
multi-method form; but it is also the object form.
240
Adapter
implementation, and that’s what the class form of the Adapter pattern
depends upon.
So, although the Adapter pattern is not language specific, there are
forms that are. It would not be possible, for example, to create the
multi-method form of the Adapter pattern in Java.
I s Th at R e a lly an A da p te r O bject?
Perhaps you think that since the only data element in the variable-light-
adapter is the :type, it is not really worthy of being called an object. OK
then, here is a different version of the variable-light-adapter that you
might find more convincing:
(ns turn-on-light.variable-light-adapter
(:require [turn-on-light.switchable :as s]
[turn-on-light.variable-light :as v-l]))
————————
(ns turn-on-light.turn-on-spec
(:require [speclj.core :refer :all]
[turn-on-light.engage-switch :refer :all]
[turn-on-light.variable-light :as v-l]
[turn-on-light.variable-light-adapter
:as v-l-adapter]))
241
Chapter 16 Design Patterns Review
(describe "Adapter"
(with-stubs)
(it "turns light on and off"
(with-redefs [v-l/turn-on-light (stub :turn-on-light)]
(engage-switch (v-l-adapter/make-adapter 5 90))
(should-have-invoked :turn-on-light
{:times 1 :with [90]})
(should-have-invoked :turn-on-light
{:times 1 :with [5]}))))
By now, you should be convinced that this is the Adapter pattern, right
out of the GOF6 book. You should also be expecting that many of the
other GOF patterns can be expressed in functional languages like
Clojure. And, perhaps more importantly, you should be thinking about
namespace/source file structures as part of the design and architecture of
functional programs.
Com m a n d
Of all the design patterns in the GOF book, Command is the one that
intrigues me the most. Not because it is complicated, but because it is
simple. Very, very simple.
6. GOF is the affectionate name we gave to the Design Patterns book back in the ‘90s. It stands for
“Gang of Four” because there were four authors: Erich Gamma, John Vlissides, Ralph Johnson,
and Richard Helm.
242
Command
class Command {
public:
virtual void execute() = 0;
};
That’s it. Just one abstract class (interface) with a single, pure, virtual
(abstract) function. So simple. But there are just so many interesting
things you can do with this pattern. For a deep dive into this richness, see
the corresponding chapter in Agile Software Development: Principles,
Patterns, and Practices.7
In a functional language like Clojure, you might think that this pattern
just disappears. After all, if you want to pass a command to some other
function, you can just pass the command function. You don’t need to make
an object out of it, because in functional languages, functions are objects:
(ns command.core)
(defn execute []
)
243
Chapter 16 Design Patterns Review
———————
(ns command.core-spec
(:require [speclj.core :refer :all]
[command.core :refer :all]))
(describe "command"
(with-stubs)
(it "executes the command"
(with-redefs [execute (stub :execute)]
(some-app execute)
(should-have-invoked :execute))))
As you can see, the test passes the execute function to some-app, and the
some-app function invokes that command. No big deal.
Now, what if you wanted to create the command with a data element
that will get passed as an argument to the execute function? In C++, we’d
do that this way (pardon the inline functions):
private:
int argument;
244
Command
(describe "command"
(with-stubs)
(it "executes the command"
(with-redefs [execute (stub :execute)]
(some-app (partial execute :the-argument))
(should-have-invoked :execute {:with [:the-argument]}))))
—————
U n do
One of the more useful variations of the Command pattern can be seen in
the following C++ code:
245
Chapter 16 Design Patterns Review
Users clicked in the palette to select the function they wanted, such
as Add a Room, and then they’d click in the canvas for placement
and size.
private:
Room* theAddedRoom;
};
246
Command
(ns command.undoable-command)
—————
(ns command.add-room-command
(:require [command.undoable-command :as uc]))
(defn add-room []
;stuff that adds rooms to the canvas
;and returns the added room
)
(defn make-add-room-command []
{:type :add-room-command})
——————
(ns command.core
(:require [command.undoable-command :as uc]
[command.add-room-command :as ar]))
247
Chapter 16 Design Patterns Review
:undo-action
(let [command-to-undo (first undo-list)]
(uc/undo command-to-undo)
(recur (rest actions)
(rest undo-list)))
:TILT))))
————————
(ns command.core-spec
(:require [speclj.core :refer :all]
[command.core :refer :all]
[command.add-room-command :as ar]))
(describe "command"
(with-stubs)
(it "executes the command"
(with-redefs [ar/add-room (stub :add-room {:return :a-room})
ar/delete-room (stub :delete-room)]
(gui-app [:add-room-action :undo-action])
(should-have-invoked :add-room)
(should-have-invoked :delete-room {:with [:a-room]}))))
248
Composite
The test stubs out the low-level functions of the add-room-command and
makes sure they are called correctly. It calls the gui-app with a list of
palette-actions.
Com posite
249
Chapter 16 Design Patterns Review
I first read about in one of Jim Coplien’s books.9 The structure of the
Composite pattern is depicted in the UML in Figure 16.6.
I
Switchable *
+ Turn on
+ Turn off
Variable Composite
Light
Light Switchable
9. James O. Coplien, Advanced C++ Programming Styles and Idioms (Addison-Wesley, 1991).
250
Composite
(ns composite-example.switchable)
—————
(ns composite-example.light
(:require [composite-example.switchable :as s]))
———————
(ns composite-example.variable-light
(:require [composite-example.switchable :as s]))
251
Chapter 16 Design Patterns Review
———————————
(ns composite-example.core-spec
(:require [speclj.core :refer :all]
[composite-example
[light :as l]
[variable-light :as v]
[switchable :as s]]))
(describe "composite-switchable"
(with-stubs)
(it "turns all on"
(with-redefs
[l/turn-on-light (stub :turn-on-light)
v/set-light-intensity (stub :set-light-intensity)]
(let [switchables [(l/make-light) (v/make-variable-light)]]
(doseq [s-able switchables] (s/turn-on s-able))
(should-have-invoked :turn-on-light)
(should-have-invoked :set-light-intensity
{:with [100]})))))
This accomplishes the goal of turning on all the lights, but it does so at
the expense of externalizing the plurality of the lights. The point of the
Composite pattern is to hide that plurality. So let’s use the actual Composite
pattern:
(ns composite-example.composite-switchable
(:require [composite-example.switchable :as s]))
(defn make-composite-switchable []
{:type :composite-switchable
:switchables []})
252
Composite
——————
(ns composite-example.core-spec
(:require [speclj.core :refer :all]
[composite-example
[light :as l]
[variable-light :as v]
[switchable :as s]
[composite-switchable :as cs]]))
(describe "composite-switchable"
(with-stubs)
(it "turns all on"
(with-redefs
[l/turn-on-light (stub :turn-on-light)
v/set-light-intensity (stub :set-light-intensity)]
(let [group (-> (cs/make-composite-switchable)
(cs/add (l/make-light))
(cs/add (v/make-variable-light)))]
(s/turn-on group)
(should-have-invoked :turn-on-light)
(should-have-invoked :set-light-intensity
{:with [100]})))))
253
Chapter 16 Design Patterns Review
methods use doseq to iterate through the :switchables list and propagate
the appropriate function call. Finally, the test creates the composite-
switchable, adds a light and variable-light, and then invokes turn-on.
And we see both lights turned on appropriately.
Fu nction a l?
At this point, you might be thinking that this is all well and good for
objects that have side effects, like lights and variable lights. Indeed, the
entire switchable interface is oriented around the side effect of turning
something on or off. So is this pattern only for objects with side effects?
(ns composite-example.shape
(:require [clojure.spec.alpha :as s]))
(ns composite-example.circle
(:require [clojure.spec.alpha :as s]
[composite-example.shape :as shape]))
254
Composite
———————
(ns composite-example.square
(:require [clojure.spec.alpha :as s]
[composite-example.shape :as shape]))
255
Chapter 16 Design Patterns Review
::top-left top-left
::side side})
Notice the :pre and :post conditions on the methods. I’m using these to
check the types coming into and going out of the functions. You could
rightly be concerned about the runtime penalty of all those checks. I’d
either globally disable10 them, or strategically comment them out once I
was happy that my types were being managed properly.
Notice that the translate and scale functions return new shape
instances. They are fully functional in their behavior.
(ns composite-example.composite-shape
(:require [clojure.spec.alpha :as s]
[composite-example.shape :as shape]))
10. There is a compile-time switch that disables all asserts, including :pre and :post.
256
Composite
(defn make []
{:post [(s/assert ::composite-shape %)]}
{::shape/type ::composite-shape
::shapes []})
For those of you who are curious, here are the tests I used:
(ns composite-example.core-spec
(:require [speclj.core :refer :all]
[composite-example
257
Chapter 16 Design Patterns Review
(describe "square"
(it "translates"
(let [s (square/make-square [3 4] 1)
translated-square (shape/translate s 1 1)]
(should= [4 5] (::square/top-left translated-square))
(should= 1 (::square/side translated-square))))
(it "scales"
(let [s (square/make-square [1 2] 2)
scaled-square (shape/scale s 5)]
(should= [1 2] (::square/top-left scaled-square))
(should= 10 (::square/side scaled-square)))))
(describe "circle"
(it "translates"
(let [c (circle/make-circle [3 4] 10)
translated-circle (shape/translate c 2 3)]
(should= [5 7] (::circle/center translated-circle))
(should= 10 (::circle/radius translated-circle))))
(it "scales"
(let [c (circle/make-circle [1 2] 2)
scaled-circle (shape/scale c 5)]
(should= [1 2] (::circle/center scaled-circle))
(should= 10 (::circle/radius scaled-circle)))))
258
Composite
(it "scales"
(let [cs (-> (cs/make)
(cs/add (square/make-square [0 0] 1))
(cs/add (circle/make-circle [10 10] 10)))
scaled-cs (shape/scale cs 12)]
(should= #{{::shape/type ::square/square
::square/top-left [0 0]
::square/side 12}
{::shape/type ::circle/circle
::circle/center [10 10]
::circle/radius 120}}
(set (::cs/shapes scaled-cs))))))
You may have noticed that as we proceed in these chapters, I’m using
more of the nuanced features of Clojure. This is intentional. I expect that
as you read this book, you will have a good Clojure reference nearby, so
I’m giving you a series of opportunities to look things up and get more
familiar with the language.
As we have seen, Composite is yet another GOF pattern that fits well into
the functional world. Once we start taking advantage of polymorphic
dispatch, with either vtables, multi-methods, or protocol/record
structures, the GOF patterns fit right in, more or less as the GOF
described them.
259
Chapter 16 Design Patterns Review
D ecor ator
For example, let’s continue with our shape project. We have a shape
type model that supports circle and square subtypes. Within that type
model, so long as it conforms to the LSP, we can translate and scale
any of the subtypes of shape without knowing the explicit subtype we
are manipulating.
260
Decorator
Enter the Decorator pattern. The UML looks like Figure 16.7.
I
Shape
*
+ translate
+ scale
Composite Journaled
Square Circle
shape shape
Journal
(ns decorator-example.journaled-shape
(:require [decorator-example.shape :as shape]
[clojure.spec.alpha :as s]))
(s/def ::journal-entry
(s/or :translate (s/tuple #{:translate}11 number? number?)
:scale (s/tuple #{:scale} number?)))
261
Chapter 16 Design Patterns Review
262
Decorator
The translate and scale functions add the appropriate journal entry to
the ::journal and then delegate their respective functions to the ::shape,
returning a new journaled-shape with the updated ::journal and the
modified ::shape.
Here’s the test. I only tested the journaled-shape with a square because if
it works for square, it will work for every shape:
Once again, I’ve included the type specifications just to give you a
challenge and to demonstrate how they can be used. Frankly, however, I
think the tests do an adequate job of checking the types; so in real life,
I doubt I would use such detailed type specifications for this kind of small
problem. On the other hand, it is kind of nice to see the types all spelled
out like that.
In any case, notice that the journaled-shape Decorator will work for any
shape, including a composite-shape. So we have effectively added a new
functionality to the type model without making any changes to the
existing element of that type model. That’s the OCP at work.
263
Chapter 16 Design Patterns Review
Vi sitor
Oh, no! Not the. . . Visitor! Yes, we’re going to investigate the much-
maligned Visitor pattern. Visitor is not one of the handle/body patterns. It
has its own unique structure that, as we’ll see, is complicated by certain
language choices.
We use the Visitor pattern when the function we wish to add is dependent
upon the subtypes in the type model.
But wait! What if one of our customers wanted the shapes in XML? I
suppose we could add a to-xml function as well as the to-string
function.
264
Visitor
But, wait again! What if another of our customers wanted the shapes in
JSON, and yet another wanted them in YAML, and. . .
At some point, you realize that there is no end to these data formats and
that customers are going to continually ask you for more and more and
more. And you don’t want to pollute the shape interface with all those
horrible methods.
The Visitor pattern gives us a way out of this dilemma. The UML looks
something like Figure 16.8.
I I
Shape Shape Visitor
+ Accept (SV) + Visit (square)
+ Visit (circle)
Circle String
visitor
Square XML
visitor
Json
visitor
YAML
visitor
The first thing I want to point out is the 90-degree rotation of the Shape
subtypes into methods in the ShapeVisitor. Each of the subtypes, Square
and Circle, is the type of the argument of a visit function in the
ShapeVisitor. I call the subtype-to-method transformation a 90-degree
rotation because it pleases some neurons in my hindbrain.
We see our Shape abstraction and all its subtypes over on the left. On the
right, we see the ShapeVisitor hierarchy. The pattern adds the accept
265
Chapter 16 Design Patterns Review
void accept(ShapeVisitor v) {
v.visit(this);
}
If you’ve never studied the Visitor pattern before, then this might be a
little difficult to follow. So take your time and walk through this with me.
Let’s say we want a JSON string for some Shape we’ve got. In Java, or
C++ or other similar languages, here’s how we’d get it:
You may have to read that over a few times to follow it. This is a technique
called double-dispatch. The first dispatch deploys to the subtype of the
Shape, so now we know the type of that subtype. The second dispatch
deploys to the proper subtype of the visitor passing along the true type of
the subtype.
If you followed all of that, you can see that each of the derivatives of the
ShapeVisitor is a new “method” of the Shape type model, but the only
266
Visitor
thing we had to add to Shape was the accept method. So ~(the OCP).
You should also now understand why we couldn’t use a Decorator. The
new functions depend strongly on the subtypes. You can’t make a JSON
string for a Square if you don’t know it’s a Square.
Now, I told you all that so I could tell you this. All that horrible
complexity is there because of a language constraint. Yes, yes. . . this is
where all those design pattern naysayers actually do have a point. The
Visitor pattern is as complex as it is because of a particular language
feature.
To C los e , or to C loju r e ?
In languages like C++ and Java, we create classes that are closed. What
that means is that we cannot add a new method to a class by putting that
new method’s declaration in a new source file. If we want to add a new
method to a class, in a closed language, we have to open the source file of
that class and add the method within the definition of that class.
Clojure does not have this constraint. Neither, to some extent, does C#.
Indeed, many languages allow you to add methods to classes without
changing the source file that contains the declaration of those classes.
The reason Clojure does not have this constraint is that classes are not a
feature of the language. We create them by convention, not by syntax.
So, wait, does that mean we don’t need the Decorator or Visitor pattern
in Clojure? No, it doesn’t mean that at all. Indeed, as we saw, we still
need the Decorator in its GOF form. How else would you do the
journaled-shape?
However, the GOF form of the Visitor is not necessary in languages that
have open classes. Or rather, some of the details of the GOF form are not
necessary.
267
Chapter 16 Design Patterns Review
So let me show you this particular Visitor in Clojure. First, the tests:
(ns visitor-example.core-spec
(:require [speclj.core :refer :all]
[visitor-example
[square :as square]
[json-shape-visitor :as jv]
[circle :as circle]]))
(describe "shape-visitor"
(it "makes json square"
(should= "{\"top-left\": [0,0], \"side\": 1}"
(jv/to-json (square/make [0 0] 1))))
Now let’s remember what the shape type model looks like. Just to keep
things simple, I’ve removed all the clojure.spec type specifications:
(ns visitor-example.shape)
———————
(ns visitor-example.square
(:require
[visitor-example.shape :as shape]))
268
Visitor
::top-left top-left
::side side})
————————
(ns visitor-example.circle
(:require
[visitor-example.shape :as shape]))
That should all look pretty familiar. Now for the json-shape-visitor:
(ns visitor-example.json-shape-visitor
(:require [visitor-example
[shape :as shape]
[circle :as circle]
[square :as square]]))
269
Chapter 16 Design Patterns Review
Just like the Java version of the Visitor, all the subtypes for the to-json
operation are gathered into the json-shape-visitor module.
If you follow all the source code dependencies and compare them to the
UML diagram, you’ll see that they are all there. The only things missing are
the ShapeVisitor interface and the dual dispatch. Those were just there to
get around the fact that languages like C++ and Java have closed classes.
This tells us that the GOF got this pattern a bit wrong. The dual dispatch
is ancillary to the Visitor pattern and is only necessary in languages with
closed classes.
12. The namespaced keyword destructuring creates a local var named for the local part of the key—
top-left in this case.
270
Visitor
I
App Shape
Json shape
visitor Circle Square
In Java, C#, and C++, we can solve this by using an abstract factory,
which the App could use to instantiate the visitor object without
depending directly upon it.
(ns visitor-example.json-shape-visitor
(:require [visitor-example
[shape :as shape]]))
271
Chapter 16 Design Patterns Review
————————
(ns visitor-example.json-shape-visitor-implementation
(:require [visitor-example
[json-shape-visitor :as v]
[circle :as circle]
[square :as square]]))
(ns visitor-example.main
(:require [visitor-example
[json-shape-visitor-implementation]]))
Typically, main is invoked before any part of the application, and thus,
the application does not have a source code dependency on main.15
Unfortunately, my tests do not have access to a true main, so the
dependency has to be included:
(ns visitor-example.core-spec
(:require [speclj.core :refer :all]
[visitor-example
272
Visitor
(describe "shape-visitor"
(it "makes json square"
(should= "{\"top-left\": [0,0], \"side\": 1}"
(jv/to-json (square/make [0 0] 1))))
I
App Shape
I
Json shape
visitor
Json shape
Square Circle
visitor
Implementation
So the Visitor pattern is a case where the GOF form was polluted by the
language constraints of the day. In 1995, when the GOF book was
published, closed classes were considered a necessary attribute of statically
typed languages and were therefore almost ubiquitous.
273
Chapter 16 Design Patterns Review
The DIP advises us to avoid source code dependencies upon things that
are both volatile and concrete. So we create abstract structures and try
to route our dependencies upon them. However, when we create instances
of objects, we often have to violate that advice; and this can cause
architectural difficulties, as shown by the UML in Figure 16.11.
I
App Shape
Circle Square
<creates>
The App in Figure 16.11 uses the Shape interface. Everything it needs to
do can be done through that interface, with one exception. The App must
create instances of the Circle and Square derivatives; and that forces the
App to hang source code dependencies upon the corresponding modules.
274
Abstract Factory
We’ve actually seen this situation in our previous examples. Consider, for
example, the code from the tests from the visitor-example earlier in this
chapter. Notice that the test requires source code dependencies upon
square and circle for the sole purpose of calling those make functions:
(ns visitor-example.core-spec
(:require [speclj.core :refer :all]
[visitor-example
[square :as square]
[json-shape-visitor :as jv]
[circle :as circle]]))
(describe "shape-visitor"
(it "makes json square"
(should= "{\"top-left\": [0,0], \"side\": 1}"
(jv/to-json (square/make [0 0] 1))))
Perhaps this seems a small price to pay. But if, as shown in Figure 16.12,
we add an architectural boundary to that UML diagram, the true cost
becomes clear.
I
App Shape
Circle Square
<creates>
Figure 16.12. Violation of the Dependency Rule across the architectural boundary
275
Chapter 16 Design Patterns Review
Here we can see that the Dependency Rule of Clean Architecture16 has been
violated by that <creates> dependency. That rule states that all source code
dependencies that cross an architectural boundary must point toward the
higher-level side of that boundary. The Circle and Square modules are low-
level details that are plug-ins to the App. Thus, to preserve the architecture,
we need to somehow deal with those <creates> dependencies.
I
Shape factory
+ Make Square
+ Make Circle
Circle Square
Shape factory
Implementation
<creates>
All the source code dependencies that cross the boundary now point toward
the higher-level side, so the Dependency Rule violation has been resolved.
The Circle and Square can still be independent plug-ins to the App. The App
can still create Circle and Square instances but indirectly through the
ShapeFactory interface, which inverts the source code dependency (the DIP).
(defmulti make-circle
(fn [factory center radius] (::type factory)))
276
Abstract Factory
(defmulti make-square
(fn [factory top-left side] (::type factory)))
—————
(ns abstract-factory-example.shape-factory-implementation
(:require [abstract-factory-example
[shape-factory :as factory]
[square :as square]
[circle :as circle]]))
(defn make []
{::factory/type ::implementation})
And with that, we can write a test that simulates our App:
(ns abstract-factory-example.core-spec
(:require [speclj.core :refer :all]
[abstract-factory-example
[shape :as shape]
[shape-factory :as factory]
[main :as main]]))
277
Chapter 16 Design Patterns Review
The first thing to notice about this test is that it has no source file
dependencies on circle or square. It depends only on the two interfaces:
shape and shape-factory. That was our architectural goal.
But what is that main dependency? Do you see the (before-all (main/
init)) line at the start of the test? That tells the test runner to call
(main/init) before any of the tests. This simulates the main module
initializing everything before starting the App.
Here’s main:
(ns abstract-factory-example.main
(:require [abstract-factory-example
[shape-factory-implementation :as imp]]))
(defn init[]
(reset! shape-factory (imp/make)))
Oh, HO! We’ve got a global atom named shape-factory! And that atom is
being initialized to the shape-factory-implementation by the init function.
So, looking back at the test, we see that the make-circle and make-square
methods were passing the dereferenced atom.
Setting a global like this is a pretty common strategy for dealing with
factories. The main program creates the concrete factory implementations
and then loads it into a global that everyone can access. In a statically typed
278
Abstract Factory
language, that global would have the type of the interface ShapeFactory. In
dynamically typed languages, no such type declaration is required.
9 0 D eg r e e s Again
Look at that UML diagram in Figure 16.13 again. Do you see the
90-degree rotation in the ShapeFactory? You can see it in the shape-
factory code too. The ShapeFactory (and the shape-factory) have
methods that correspond to the subtypes of Shape.
The problem that this caused for Visitor is also present here, although in
a slightly different form. Whenever a new subtype of shape is added, the
shape-factory must be modified. That violates the OCP because we must
modify a module on the high-level side of the architectural boundary. If
the OCP matters at all, it matters most especially across such boundaries.
Study that UML diagram until you see what I mean.
(ns abstract-factory-example.shape-factory)
——————————
(ns abstract-factory-example.shape-factory-implementation
(:require [abstract-factory-example
[shape-factory :as factory]
[square :as square]
[circle :as circle]]))
(defn make []
{::factory/type ::implementation})
279
Chapter 16 Design Patterns Review
————————————————
(ns abstract-factory-example.core-spec
(:require [speclj.core :refer :all]
[abstract-factory-example
[shape :as shape]
[shape-factory :as factory]
[main :as main]]))
280
Conclusion
This opacity is the key to this solution. If we ever need to add a triangle
subtype, nothing above the boundary line will have to change (the OCP).
Ty pe Sa fet y?
In a statically typed language, like Java, this technique abandons type
safety. Opaque values cannot be type safe. There is no way, for example,
to use an enum in Java to solve this issue.
In Clojure, we aren’t concerned about static type safety, but what about
dynamic type specifications? We’re out of luck there too. There is no way
to gain an advantage by using clojure.spec since all errors, either with or
without clojure.spec, will be runtime errors.
There is no escape from this in any language. Whether in Java, C++, Ruby,
Clojure, or C#, if you want to maintain the OCP across architectural
boundaries (and you usually do), then at some point across that boundary
you are going to have to abandon type safety and rely on runtime
exceptions. This is just simply software physics.
Conc lusion
I’ll leave the rest of the GOF patterns, and any other patterns you might be
familiar with, as an exercise. By now, I’m pretty sure you understand that
functional languages that have facilities similar to Clojure are as OO as
Java, C#, Ruby, and Python, and that the patterns described in the GOF
book generally apply so long as the constraint of immutability is enforced.
281
Chapter 16 Design Patterns Review
I thought it wise to revisit here my hope and goal from the introduction.
By now, it should be clear that functional programming and OOP are
compatible and mutually beneficial styles.
The design pattern examples that I have presented so far are not unusual.
Clojure programmers frequently use defmulti and defmethod to express
polymorphism. They typically use maps to express encapsulated data
structures (i.e., objects). They often even build constructors for those
objects. They might not realize it, but they are building OO programs.
282
Postscript: OO Poison?
It should be very clear by now that Clojure is every bit as object oriented
as Java, C++, C#, Python, and Ruby. Clojure is also as functional as F#,
Scala, Elixir, and (dare I say it?) Haskell.
Clojure does not have inheritance; but it does have at least three very
effective mechanisms of polymorphism. At least two of those mechanisms
support open classes.
Clojure supports, but does not enforce, a source file and namespace
structure that affords the same architectural partitioning we find so
familiar in any of the (so-called) enterprise languages.
We can still describe our functional programs using interfaces and classes,
types and subtypes. We can still partition the source files and manage
their dependencies in order to create robust, independently deployable
and independently developable architectures. Nothing in that regard has
changed at all.
283
Chapter 16 Design Patterns Review
of side effects. Our classes and modules will strongly prefer immutable, as
opposed to mutable, objects. But they are still objects, and they can still be
expressed and organized as classes that implement interfaces.
And that means that the vast majority of the design principles and design
patterns that we found so helpful in OO languages still apply, and are
still useful, in functional languages like Clojure and others.
284
VI
Case Study
285
This page intentionally left blank
17
Wa-Tor
287
Chapter 17 Wa-Tor
In the final chapter of this book, you and I are going to play a little game
about a little game. The little game our little game will be about
is called Wa-Tor; a simple little cellular automaton described by A. K.
Dewdney in the December 1984 issue of Scientific American.1 The game
you and I are going to play is to pretend that Wa-Tor is an enterprise-
level application requiring significant effort in architecture and design.
I mean, honestly, I could hack together Wa-Tor in a few hours and walk
away happy. But for this chapter, I want us to really think about the
issues as though this were a 50 mega line of code (LOC) monster.
The world that the fish and sharks live in has no land; it’s all water.
Moreover, the top meets the bottom and the left meets the right, so the
world is topologically a torus. Thus, Wa-Tor stands for WAter TORus.
We’ll talk more about the features of the program later. For the moment,
what are the architectural and design considerations?
Let’s start with the basics. SRP. Who are the actors—whom do we want
to keep separate?
In most large enterprise systems, there are many different actors. But in this
little app, there are only two to worry about. There are the user experience
(UX) designers, who will undoubtedly change their minds a dozen or so
288
Wa-Tor
times before they actually like what they see on the screen. And then there
are the modelers who will also likely fiddle with the internal shark/fish
behavior and might possibly add more animals to the mix.
So we start out with Figure 17.1, a very obvious and very traditional
partitioning.
Wa-tor Wa-tor
UI model
There are only two components4 and one boundary in this partitioning
so far. In larger systems, we would see many more boundaries and many
more components within each.
Let’s focus on the model first.5 What kinds of classes are we going
to need?
So, at first blush, I think the object model looks something like
Figure 17.2.
3. The definition of high and low “level” that I’m using here is “distance from I/O.” See Robert C.
Martin, Clean Architecture (Pearson, 2017), p. 183.
4. See Martin, Clean Architecture, p. 93.
5. http://wiki.c2.com/?ModelFirst
289
Chapter 17 Wa-Tor
A
* Cell
World
+ Tick
A
Animal
Water + Move
+ Reproduce
Shark
Fish
+ Eat
The world contains a bunch of cells. Each cell can process a tick6 of
time. I guessed that cell is abstract rather than an interface because I
expect that there will be concrete functions at this level.
Each cell can be water, or an animal that can move and reproduce. The
two possible subtypes of animal are fish and sharks that can eat.
Let’s see if we can code this. No tests yet, because we haven’t defined any
behavior:
(ns wator.cell)
——————
(ns wator.water
(:require [wator
[cell :as cell]]))
290
Wa-Tor
———————
(ns wator.animal)
—————
(ns wator.fish
(:require [wator
[cell :as cell]
[animal :as animal]]))
—————
(ns wator.shark
(:require [wator
[cell :as cell]
[animal :as animal]]))
291
Chapter 17 Wa-Tor
This looks pretty standard. The cell module looks like an interface so
far. The water module implements it trivially. The dangling parentheses
are there to remind me that I want to add something to that function.
The animal module does not implement tick, but it does have a function
named tick that can be called by its subtypes. I put this in as a guess. It’s
a bit of hubris, I suppose; but I have a feeling that it’ll be necessary.7
The fish trivially implements both the cell and animal. This actually
looks more like multiple inheritance than the UML diagram. On the
other hand, there’s no inheritance anywhere in this code, so. . .
Finally, shark also trivially implements both cell and animal and adds its
own eat function.
I didn’t code the world because I don’t know enough to even start.
However, there are a few issues that I think the world will have to deal
with. We don’t want the world to depend upon the GUI, and yet the GUI
is going to put a lot of constraints on the world. For example, it seems to
me that the GUI is going to tell us the size of the world. I also think that
since the GUI is likely to repaint the screen N times per second, the GUI
will define time.
7. Yeah, I know. You Aren’t Gonna Need It (YAGNI). Well, we’ll see.
292
Wa-Tor
But let’s set all that aside for the time being. Enough of this up-front
design. Let’s see if we can code some of the behavior.
What is the behavior of water? We ask our modelers, and they tell us that
a water cell will randomly evolve into a fish cell if given enough time.
Here’s my implementation of that rule:
(ns wator.core-spec
(:require [speclj.core :refer :all]
[wator
[cell :as cell]
[water :as water]
[fish :as fish]]))
(describe "Wator"
(with-stubs)
(context "Water"
(it "usually remains water"
(with-redefs [rand (stub :rand {:return 0.0})]
(let [water (water/make)
evolved (cell/tick water)]
(should= ::water/water (::cell/type evolved)))))
———
(ns wator.water
(:require [wator
[cell :as cell]
[fish :as fish]
[config :as config]]))
293
Chapter 17 Wa-Tor
——————
(ns wator.config)
So, right away we see the “functional” nature of this program.8 The
return value of tick is a new cell. I don’t know if that water-evolution-
rate is correct. The modelers haven’t told us what the rate should be. So
I just guessed. I expect that they’ll wait until they see how the model
behaves and then tell us to change it.
So far, I haven’t specified any dynamic types. It seems a bit early for that.
But I’m pretty sure it’s coming.
Wait. How do you move a fish? Where is the fish? Does the fish know
its location, or is that something the world knows?
I like using maps for things like this, so let’s make a world full of water cells:
(context "world"
(it "creates a world full of water cells"
(let [world (world/make 2 2)
294
Wa-Tor
————
(ns wator.world
(:require [wator
[water :as water]]))
(defn make [w h]
(let [locs (for [x (range w) y (range h)] [x y])
loc-water (interleave locs (repeat (water/make)))
cells (apply hash-map loc-water)]
{:cells cells}))
Did you catch the use of the lazy list of water cells passed into interleave?
Now we should be able to put a fish in the world and move it around.
Here’s my first try at a test:
(context "animal"
(it "moves"
(let [fish (fish/make)
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[loc cell] (animal/move fish [1 1] world)]
(should= cell fish)
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc)))))
295
Chapter 17 Wa-Tor
I made a ton of design decisions while composing this test. Those kinds of
decisions are why the last D in TDD often stands for design. I’ll walk
you through those decisions in a moment, but first let me show you the
code that passes this test:
(ns wator.world
(:require [wator
[water :as water]]))
(defn make [w h] . . .)
——————
(ns wator.animal
(:require [wator
[cell :as cell]]))
——————
(ns wator.fish
(:require [wator
[cell :as cell]
[animal :as animal]]))
296
Wa-Tor
When you see . . . in a method body, it means that there has been no
change to that method since the last time I presented it.
So, on to the design decisions that I made while composing this test. My
first problem was that an animal can’t move if it can’t see the world. So
either every animal should hold a reference to the world, or the world
should be a global atom, or the world should be passed in as an argument
to the move function. I chose the latter because I feel a kind of mild
disdain10 for abandoning the functional paradigm and falling back on
atoms and STM.
My next problem was that the animal does not know its location. So I
need to pass the location of the animal into the move function along with
the world.
Finally, and most importantly, I puzzled over what the move function
should return. At first, I thought it should return the updated world. But
this creates the following inconsistency problem.
9. This is kind of like implementing a method in a base class and allowing subclasses to either over-
ride it or not.
10. Perhaps that disdain is misplaced, but this IS a book about functional design, so. . .
297
Chapter 17 Wa-Tor
Imagine the update process for the world. It begins at location [0 0] and
walks through the world updating each cell in turn. Now imagine there
is a fish at [0 0] and that the update moves it to [0 1]. But [0 1] is the
cell that the world updates next. So that same fish moves again. A fish
should not move twice in a single turn.
So the move function cannot update the world. Instead, the world is going
to have to build up a new world from the old world, one cell at a time. I
imagine we could do it something like this:11
destinations (filter
#(water/is?
(world/get-cell world %))
neighbors)
new-location (rand-nth destinations)]
[new-location animal]))
Very pretty. We ask the world for the neighbors of the location, filter out
any that aren’t water, and then randomly choose one. Cool.
11. Remember that :cells holds a map, so the update-cell function will take [key val] pairs and
return [key val] pairs.
298
Wa-Tor
I thought it best to make sure that all the torus math was nicely sequestered
within world. I didn’t want it leaking out into all the animals:
Are you ready for the stuff that’s not pretty? The code above refused to
compile, because (are you ready for this?) water depends upon fish (for the
evolution), fish depends upon animal (for do-move), and animal depends
upon water. That’s a dependency cycle, and Clojure hates dependency cycles.
See Figure 17.3.
A
* Cell
World
+ Tick
A
Animal
Water + Move
+ Reproduce
Shark
Fish
+ Eat
299
Chapter 17 Wa-Tor
So the way we have to solve this is by falling back on something like the
old C mechanism of declarations and implementations. See Figure 17.4.
A
Cell
World
+ Tick
A
Animal
Water + Move
+ Reproduce
Water
Imp
Shark
Fish
+ Eat
Fish Shark
Imp Imp
12. Actually, just fish. I split shark on the diagram but not in the code. YAGNI, YAGNI, YAGNI.
13. Future Uncle Bob: . . .nope.
300
Wa-Tor
(ns wator.world
(:require [wator
[water :as water]]))
(defn make [w h]
(let [locs (for [x (range w) y (range h)] [x y])
loc-water (interleave locs (repeat (water/make)))
cells (apply hash-map loc-water)]
{::cells cells
::bounds [w h]}))
; . . .
—————
(ns wator.cell)
—————
(ns wator.water
(:require [wator
[cell :as cell]]))
——————————
301
Chapter 17 Wa-Tor
(ns wator.water-imp
(:require [wator
[cell :as cell]
[water :as water]
[fish :as fish]
[config :as config]]))
———————
(ns wator.animal
(:require [wator
[world :as world]
[cell :as cell]
[water :as water]]))
————
(ns wator.fish
(:require [wator
[cell :as cell]]))
302
Wa-Tor
——————
(ns wator.fish-imp
(:require [wator
[cell :as cell]
[animal :as animal]
[fish :as fish]]))
The criterion for splitting water and fish is pretty easy to see. Any function
that references a file outside of the direct type hierarchy gets put into the
imp file. Pay special attention to the namespaces and the namespaced
keywords. For example, notice that the defmethods in fish-imp will still be
dispatched on ::fish/fish.
And just in case you thought I’d forgotten, here are the current tests:
(ns wator.core-spec
(:require [speclj.core :refer :all]
[wator
[cell :as cell]
[water :as water]
[water-imp]
[animal :as animal]
[fish :as fish]
[fish-imp]
[world :as world]]))
303
Chapter 17 Wa-Tor
(describe "Wator"
(with-stubs)
(context "Water"
(it "usually remains water"
(with-redefs [rand (stub :rand {:return 0.0})]
(let [water (water/make)
evolved (cell/tick water)]
(should= ::water/water (::cell/type evolved)))))
(context "world"
(it "creates a world full of water cells"
(let [world (world/make 2 2)
cells (::world/cells world)
positions (set (keys cells))]
(should= #{[0 0] [0 1]
[1 0] [1 1]} positions)
(should (every? #(= ::water/water (::cell/type %))
(vals cells)))))
304
Wa-Tor
[0 3] [0 4] [0 0]]
(world/neighbors world [4 4])))))
(context "animal"
(it "moves"
(let [fish (fish/make)
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[loc cell] (animal/move fish [1 1] world)]
(should= cell fish)
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc))))))
OK, now that we can move the fish, I’m pretty sure the sharks will move
too. So next we should try some reproduction. But before we do that, I’m
getting (pretend) concerned about the type system for the world. Let’s get
that set up first:
(ns wator.world
(:require [clojure.spec.alpha :as s]
[wator
[cell :as cell]
[water :as water]]))
(defn make [w h]
{:post [(s/valid? ::world %)]}
…)
305
Chapter 17 Wa-Tor
OK, that’s better. Now, what do we need for reproduction? The modelers
said that a fish will reproduce if it is next to a water cell and is above
a certain age. The two daughter fish have their ages reset to zero.
Otherwise, the ::age of a fish increases with time.
(it "reproduces"
(let [fish (-> (fish/make)
(animal/set-age config/fish-reproduction-age))
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[loc1 cell1 loc2 cell2] (animal/reproduce
fish [1 1] world)]
(should= loc1 [1 1])
(should (fish/is? cell1))
(should= 0 (animal/age cell1))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc2))
(should (fish/is? cell2))
(should= 0 (animal/age cell2))))
306
Wa-Tor
(world/set-cell [1 1] fish))
failed (animal/reproduce fish [1 1] world)]
(should-be-nil failed)))
Notice that if the fish reproduces, the return value contains both
daughters. But if something goes wrong, we return nil. This is because I
reckon that the high-level policy of a fish includes something like this:
(ns wator.animal
(:require [clojure.spec.alpha :as s]
[wator
[world :as world]
[cell :as cell]
[water :as water]
[config :as config]]))
(defn make []
{::age 0})
;. . .
307
Chapter 17 Wa-Tor
————
(ns wator.fish
(:require [clojure.spec.alpha :as s]
[wator
[cell :as cell]
[animal :as animal]]))
(defn make []
{:post [(s/valid? ::fish %)]}
(merge {::cell/type ::fish}
(animal/make)))
——————
(ns wator.fish-imp
(:require [wator
[cell :as cell]
308
Scratch That Itch
;. . .
S c r atc h Th at Itc h
I’m getting an itchy feeling that I should have implemented world/tick
first. I’ve made a lot of decisions about the return values of move and
reproduce based upon what I think world/tick is going to need. So let’s
switch gears and focus on that before we continue to add more, possibly
errant, goop to the animals.
14. Yeah, I know, YAGNI and all that. But rules are meant to be broken.
309
Chapter 17 Wa-Tor
It’s pretty simple. We make a small-world with two cells, one of which is
a fish. We call tick on that world, and then we make sure that the fish
moves to the vacant cell and that it leaves water behind.
Next, I wrote a dummy implementation for tick, just to see the test pass:
Lo and behold, this won’t compile because world now depends upon
fish, which depends upon animal, which depends back upon world. Sigh.
Cyclic dependencies are the bane of source code structures that are
thought through poorly.
A A
World Cell
+ Tick = 0 + Tick
A
Animal
Water + Move
+ Reproduce
Water
Imp
Shark
Fish
+ Eat
World Imp
+ Tick
Fish Shark
Imp Imp
310
Scratch That Itch
(ns wator.world
(:require [clojure.spec.alpha :as s]
[wator
[cell :as cell]
[water :as water]]))
(defn make [w h]
{:post [(s/valid? ::world %)]}
(let [locs (for [x (range w) y (range h)] [x y])
loc-water (interleave locs (repeat (water/make)))
cells (apply hash-map loc-water)]
{::type ::world
::cells cells
::bounds [w h]}))
; . . .
—————
(ns wator.world-imp
(:require [wator
[world :as world :refer :all]
[animal :as animal]
[fish :as fish]
[water :as water]]))
311
Chapter 17 Wa-Tor
This passed the test once I added [world-imp] to the :require list in the test.
Take note that tick is now a multi-method with only one implementation.
That’s the dependency inversion that we needed.
312
Showers Solve Problems
A A
World Cell
+ Tick = 0 + Tick
+ MakeCell = 0
A
Animal
Water + Move
+ Reproduce
Water
Imp
Shark
Fish
+ Eat
World Imp
+ Tick
+ MakeCell Fish Shark
Imp Imp
(ns wator.world
(:require [clojure.spec.alpha :as s]
[wator
[cell :as cell]
[water :as water]]))
313
Chapter 17 Wa-Tor
(defn make [w h]
{:post [(s/valid? ::world %)]}
(let [locs (for [x (range w) y (range h)] [x y])
default-cell (make-cell ::world :default-cell)
loc-water (interleave locs (repeat default-cell))
cells (apply hash-map loc-water)]
{::type ::world
::cells cells
::bounds [w h]}))
;. . .
———————
(ns wator.world-imp
(:require [wator
[world :as world :refer :all]
[animal :as animal]
[fish :as fish]
[shark :as shark]
[water :as water]]))
I have high hopes for this change. And please note, this whole change was
driven by one test that I made to pass using a dummy implementation in
tick, reminding us yet again that TDD is a design technique.
314
Showers Solve Problems
OK, now let’s make that dummy implementation fail. Here’s the test
that fails:
I created the four possible 1-by-2 scenarios and made sure the world got
updated properly after a tick.
Making this pass forced me to change the design yet again. The animal/
move, animal/reproduce, and cell/tick functions must return a [from to]
list in which each is a single-element map containing {loc cell}. Look at
the world-imp and you’ll see why:
(ns wator.world-imp
. . .)
315
Chapter 17 Wa-Tor
(empty? locs)
(assoc world ::world/cells new-cells)
:else
(let [loc (first locs)
cell (get cells loc)
[from to] (cell/tick cell loc world)
new-cells (-> new-cells (merge from) (merge to))
to-loc (first (keys to))]
(recur (rest locs)
new-cells
(conj moved-into to-loc)))))))
; . . .
It turns out that every operation makes changes to either one or two cells.
When an animal moves, reproduces, or eats, only two cells are involved.
If an animal fails to move, or if it starves, only one cell is involved. In the
first case the operation will return [from to], and in the second case it
will return [nil to]. In either case, both from and to are merged15 into
new-cells.
316
Showers Solve Problems
Quite a few changes had to be made throughout the structure to get this
to work. So my “itch” from a few pages back was correct. It’s a good
thing I paid attention to it early enough to make the change doable:
(ns wator.cell)
——————
(ns wator.water-imp
(:require [wator
[cell :as cell]
[water :as water]
[fish :as fish]
[config :as config]]))
————
(ns wator.animal . . .)
;. . .
317
Chapter 17 Wa-Tor
;. . .
————
(ns wator.fish-imp . . .)
; . . .
(ns wator.core-spec . . .)
(describe "Wator"
(with-stubs)
(context "Water"
(it "usually remains water"
(with-redefs [rand (stub :rand {:return 0.0})]
(let [water (water/make)
world (world/make 1 1)
[from to] (cell/tick water [0 0] world)]
(should-be-nil from)
(should (water/is? (get to [0 0])))
)))
318
Showers Solve Problems
;. . .
(context "animal"
(it "moves"
(let [fish (fish/make)
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[from to] (animal/move fish [1 1] world)
loc (first (keys to))]
(should (water/is? (get from [1 1])))
(should (fish/is? (get to loc)))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc))))
There’s another scenario that I think will fail—two fish competing for
the same spot:
(it "move two fish who compete for the same spot"
(let [fish (fish/make)
competitive-world (-> (world/make 3 1)
(world/set-cell [0 0] fish)
319
Chapter 17 Wa-Tor
(world/set-cell [2 0] fish)
(world/tick))
start-00 (world/get-cell competitive-world [0 0])
start-20 (world/get-cell competitive-world [2 0])
end-10 (world/get-cell competitive-world [1 0])]
(should (fish/is? end-10))
(should (or (fish/is? start-00)
(fish/is? start-20)))
(should (or (water/is? start-00)
(water/is? start-20)))))
A simple 3-by-1 world with fish at either end. Only one of them can
move into the center slot. The other will have to remain where it was.
This test fails because the animal/move function does not know that a
fish already moved into the target slot.
(ns wator.world-imp . . .)
:else
(let [loc (first locs)
cell (get cells loc)
320
Showers Solve Problems
———————
(ns wator.animal . . .)
; . . .
Note that I did not use a namespaced keyword for :moved-into. That’s
because I consider it to be tramp data that is not really part of the world
and is just kind of hitching a ride. This feels a little dirty, but it works.16
Note that we only put locations into moved-into if the cell being moved
in is not water.
321
Chapter 17 Wa-Tor
Nifty. Create a 10-by-10 world. Load it with one fish. Send it 100 ticks,
and make sure there are more than 50 fish. I mean, the fish are moving
around and reproducing like crazy in there!
Of course, this test fails; but only because we didn’t call reproduce in
animal/tick. So let’s fix that:
Yup. Age the animal, then see if it will reproduce. If not, then move it.
Simple. Easy.
Of course, I had to fix the fact that reproduce didn’t use our new [from to]
convention:
322
It’s Time to Wildly Reproduce17
(it "reproduces"
(let [fish (-> (fish/make)
(animal/set-age config/fish-reproduction-age))
world (-> (world/make 3 3)
(world/set-cell [1 1] fish))
[from to] (animal/reproduce fish [1 1] world)
from-loc (-> from keys first)
from-cell (-> from vals first)
to-loc (-> to keys first)
to-cell (-> to vals first)]
(should= from-loc [1 1])
(should (fish/is? from-cell))
(should= 0 (animal/age from-cell))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
to-loc))
(should (fish/is? to-cell))
(should= 0 (animal/age to-cell))))
But with that, the fish reproduce like. . . fish. That was pretty easy. I
think our design is coming together.
323
Chapter 17 Wa-Tor
Wh at a bout the S h ar k s ?
I’ve neglected the shark class so far because its behavior is almost
identical to fish and is mostly governed by the animal abstraction. But
now let’s see if we can get shark objects to move and reproduce.
This required me to flesh out the shark module and also make one small
design change. I used the Template Method pattern to get the reproduction
age of an animal. The tests hint at that change:
(context "animal"
(it "moves"
(doseq [scenario
[{:constructor fish/make :tester fish/is?}
{:constructor shark/make :tester shark/is?}]]
(let [animal ((:constructor scenario))
world (-> (world/make 3 3)
(world/set-cell [1 1] animal))
[from to] (animal/move animal [1 1] world)
loc (first (keys to))]
(should (water/is? (get from [1 1])))
(should ((:tester scenario) (get to loc)))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
loc)))))
324
What about the Sharks?
(it "reproduces"
(doseq [scenario
[{:constructor fish/make :tester fish/is?}
{:constructor shark/make :tester shark/is?}]]
(let [animal ((:constructor scenario))
reproduction-age (animal/get-reproduction-age animal)
animal (animal/set-age animal reproduction-age)
world (-> (world/make 3 3)
(world/set-cell [1 1] animal))
[from to] (animal/reproduce animal [1 1] world)
from-loc (-> from keys first)
from-cell (-> from vals first)
to-loc (-> to keys first)
to-cell (-> to vals first)]
(should= from-loc [1 1])
(should ((:tester scenario) from-cell))
(should= 0 (animal/age from-cell))
(should (#{[0 0] [0 1] [0 2]
[1 0] [1 2]
[2 0] [2 1] [2 2]}
to-loc))
(should ((:tester scenario) to-cell))
(should= 0 (animal/age to-cell)))))
325
Chapter 17 Wa-Tor
————————
(ns wator.animal …)
; . . .
————————
(ns wator.fish . . .)
; . . .
——————
(ns wator.shark
(:require [clojure.spec.alpha :as s]
[wator
[config :as config]
[cell :as cell]
[animal :as animal]]))
326
What about the Sharks?
(defn make []
{:post [(s/valid? ::shark %)]}
(merge {::cell/type ::shark}
(animal/make)))
; . . .
So far, with the exception of the reproduction age, the behavior of both
the shark and fish is “inherited” from (actually it is delegated to) animal.
But the shark class has extra constraints that we need to implement now.
The modelers have told us that a shark only reproduces if its :health is
above a certain threshold. The :health of a shark is increased by eating a
fish, and it decreases with time. If the :health of a shark reaches zero,
the shark starves, leaving behind water. When a shark reproduces, its
:health is split between the two daughters.
(context "shark"
(it "starts with some health"
(let [shark (shark/make)]
(should= config/shark-starting-health
(shark/health shark))))
327
Chapter 17 Wa-Tor
(world/set-cell [0 0] (shark/make)))
aged-world (world/tick small-world)
aged-shark (world/get-cell aged-world [0 0])]
(should= (dec config/shark-starting-health)
(shark/health aged-shark)))))
—————
(ns wator.shark . . .)
(defn make []
{:post [(s/valid? ::shark %)]}
(merge {::cell/type ::shark
::health config/shark-starting-health}
(animal/make)))
; . . .
Pretty easy. We just added the ::health field to the ::shark spec and
shark/make, and then we decremented the ::health in the tick function
just before delegating the rest of the behavior to the superclass animal.
328
What about the Sharks?
Now let’s test that a shark will die when its ::health goes to zero:
————
(ns wator.shark . . .)
; . . .
Pretty easy. OK, so now let’s test that sharks will eat when given the
opportunity:
329
Chapter 17 Wa-Tor
We create a 2-by-1 world with a shark next to a fish. After one tick,
the shark should be where the fish was, and water should be where the
shark was, and the shark’s ::health should have increased.
(ns wator.shark . . .)
330
What about the Sharks?
All this slipped in with little hassle. We’ve passed through the design
bottleneck and are now reaping the benefits.
The modelers told us that a shark will only reproduce if its health is
above a threshold. Let’s test that. In fact, let’s make that change first18
and see which tests break:
(ns wator.shark . . .)
As expected, the test for animal reproduction fails in the shark scenario.
We can address this by putting a little hack in that test:
(it "reproduces"
(doseq [scenario [{:constructor fish/make :tester fish/is?}
{:constructor
#(-> (shark/make)
(shark/set-health
(inc config/shark-reproduction-
health)))
:tester shark/is?}]]
; . . .
Yes, that’s a bit ugly, but it does the job. I suppose I should add a test for
checking the other side of that threshold:
331
Chapter 17 Wa-Tor
(dec config/shark-reproduction-health))
(animal/set-age config/shark-reproduction-age))
world (-> (world/make 3 3)
(world/set-cell [1 1] shark))
failed (animal/reproduce shark [1 1] world)]
(should-be-nil failed)))
OK. One last thing. The health of the parent shark is split between the
two daughter sharks:
Yup. That fails because the expected health isn’t correct. That should be
simple to fix:
(ns wator.shark . . .)
332
What about the Sharks?
And with that, I think the model is complete. Let’s see if we can put a GUI
on top of it:
(ns wator-gui.main
(:require [quil.core :as q]
[quil.middleware :as m]
[wator
[world :as world]
[water :as water]
[fish :as fish]
[shark :as shark]
[world-imp]
[water-imp]
[fish-imp]]))
(defn setup []
(q/frame-rate 60)
(q/color-mode :rgb)
(-> (world/make 80 80)
(world/set-cell [40 40] (fish/make)))
)
333
Chapter 17 Wa-Tor
(declare wator)
Yeah, that wasn’t too hard. Figure 17.7 is a screenshot of the game in
progress.
It’s not super-fast; but that’s not a big surprise. There are a bunch of
things we could do to speed it up. But never mind that. Look at that
GUI code. It depends on the model, yet the model knows nothing of
the GUI. And that satisfies our original architectural goal.
334
Conclusion
Conc lusion
Wa-Tor is a program that is “functional”19 and object oriented; complete
with several OO design patterns right out of the GOF book. Indeed, it
was the OO partitioning that helped the design congeal so nicely.
The OO partitioning separates and isolates the various data types very
nicely, and it provides pleasant locations for the related functions. Any OO
programmer would be very comfortable with this.
19. Why the quotes? Because random numbers aren’t referentially transparent, so this program is not
purely functional.
335
Chapter 17 Wa-Tor
However, at its heart, this is a data flow model. The world flows through
the behaviors in the various objects, without any mutation. The plumbing
model of functional programming still holds.
In the end, I think this is the way software was meant to be.
By the way, you can find all the source code at https://github.com/unclebob
/wator.
336
A fterword
All the little bits in my brain frantically searched for an answer while I
was simultaneously trying to understand what he was asking me, until
finally I very unconfidently answered, “Clojure?”
In shock, he continued . . . “Front end and back end all in Clojure? I’ve
never heard of that before. How does that work? Clojure is a Lisp
language, right? It’s functional.”
337
Afterword
Yes, it is, but Oh no! Another question . . . “How does that work?”
Well, if you’re reading this, then I assume you’ve read the preceding pages
and thus have already received a much better and more elaborate
explanation than I could offer you here, so let’s address the elephant in
the room: Why was asking me my preferred stack a bomb of a question?
I immediately had questions because I’d only ever known the basics of
languages like Java and Python. He explained the basic differences of OO
procedural languages and functional languages and why he liked Clojure.
In one example, he showed me why functional languages are “safer”
and less complicated than those that rely heavily on mutable states by
depicting a race condition for me that was almost identical to that of the
phone call between Bob and Alice found in Chapter 15.
338
Afterword
He walked me through Quil too, and how even it was mostly functional
and how instead of changing a state, it simply recurred a new state at
each iteration. This went a little over my head at the time, but I fell back
on this conversation a lot over the next year—I even have in front of me
right now the printout of the source code we’d written that night as
inspiration for writing this.
So, back to the elephant: As of March 2022, I was still pretty new to
software; due to COVID-19, there hadn’t been many large-group events
that had taken place yet; and because Baton Rouge, LA, has some
opportunity for growth in the software sector, I had been pretty isolated
as a developer and had experienced little exposure to common industry
lingo.
With that out of the way, I’ll leave you with two final tidbits.
339
Afterword
Then they asked for a mobile application using all the same functionality
as our Clojure features. We extracted much of the core functionality into
a cljc library, and from there we were able to build the mobile app with
little to no code duplication or rewrites.
We used common functions for the cljs mobile application, as we did
for the back end, by utilizing Clojure common namespaces.
In how many languages can you say you’ve done that—had the back
end and, potentially, multiple front ends all functioning on the same,
simultaneously tested code?
2. This got me, as I’ve seen it get others, and if you’re used to OO it will
probably get you. for in Clojure is not a loop. It is a list comprehension
macro, and it does not force side effects. Instead, use doseq, which
returns nil but will accomplish what you are incorrectly trying to
achieve with for.
Good luck!
340
I nde x
341
Index
architectural boundaries C
maintaining OCP across, 281 cathode ray tubes (CRTs), 118
in Payroll example, 98, 100, 105 change requests and Single
in shape example, 271, 273, Responsibility Principle,
275–276 126–131
in Wa-Tor app, 289, 312–313 Church, Alonzo, xvi–xvii
architecture. See Dependency Rule Church-Turing thesis, xvi–xvii, 19
of Clean Architecture classes
arrays closed, 267, 273
and n-ary trees, 23–25 and interfaces, 132, 283–284
in Sieve of Eratosthenes vs. namespaces, 107
algorithm, 20–22 Clean Architecture (Martin),
assignment 96, 126
defined, 7–8 Cleancoders, 126, 213
programming without, 4–6, Clean Craftsmanship (Martin),
20–22 53, 79n
assoc, 90n5 Clojure
async/>!! function, 213 and classes, 267
async/go function, 205 compiling, 137, 150, 256n, 300
atom (atomic value), 50–51 features and constraints, 41,
atomic operations, 50 281, 282–284
interface segregation, 149–150
B keyword syntax, 45n
batch vs. interactive on learning, xvii–xviii
applications, 44
namespaces and source files,
behavioral cohesion, 94, 336 107–108
binary trees. See n-ary trees source code dependency,
Bowling Game problem 104–105
Clojure version, 71–75 syntax overview, 29–32
comparison of solutions, clojure.spec library, 47, 110
75–76 cogency, 151
Java version, 66–71 cohesion, 94
business rules Command pattern
and dependencies, 154–155 Undo variation, 245–249
tests, 127–131 usage, 242–245
342
Index
343
Index
344
Index
345
Index
346
Index
347
Index
348
Index
threads variables
and race conditions, 216 assignment, 7–8
usage, 9, 48–51 in functional programs, 15–16,
TILT error message, 122n 18–19
Turing, Alan, xvi–xvii Video Store problem, 155–165,
Turing machines, xvi–xvii, 19 166–179, 190–197
turtle graphics Visitor pattern
command handling, 210–212 in shape example, 264–267,
framework and drawing 268–270, 271–273
functions, 202–210 usage, 264
usage, 200–202 vtables, 134, 139, 235
turtles (printing devices), 200–201
W
type integrity
Wa-Tor app
with clojure.spec library,
10x10 test, 322–323
110–113
actors, 288–289
with :pre and :post, 113–114
Factory Method solution,
type models, and Decorator
312–322
pattern, 260–263
fish movement behavior,
type safety, 281
295–305
types. See also duck typing
fish reproduction behavior,
in Liskov Substitution 305–309
Principle, 139
as functional and object
U oriented, 335–336
Undo Command pattern, 245–249 game concept, 288
unified modeling language (UML) objects, 289–292
diagrams, 97 screenshot, 335
union function, 91n8 shark class, 324–334
update function, 91n7 water behavior, 293–295
world dependency, 309–312
V
vals, 91n9 Y
YAGNI (You Aren’t Gonna Need
It), 292n
349
This page intentionally left blank
Register Your Product at informit.com/register
Access additional benefits and save up to 65%* on your next purchase
• utomatically receive a coupon for 35% off books, eBooks, and web editions and
A
65% off video courses, valid for 30 days. Look for your code in your InformIT cart
or the Manage Codes section of your account page.
• Download available product updates.
• Access bonus material if available.**
• heck the box to hear from us and receive exclusive offers on new editions
C
and related products.
Addison-Wesley • Adobe Press • Cisco Press • Microsoft Press • Oracle Press • Peachpit Press • Pearson IT Certification • Que