Programming Well - Harvard CS51
Programming Well - Harvard CS51
SHIEBER
PROGRAMMING
WELL:
ABSTRACTION AND
DESIGN IN
C O M P U TAT I O N
2
1
3
©2022 Stuart M. Shieber. All rights reserved for the time being, though
the intention is for this document to eventually be licensed under a CC
license. In the meantime, please do not cite, quote, or redistribute.
Commit 1e68c0c from Wed Nov 16 14:13:28 2022 -0500 by Stuart Shieber.
Preface
This book began as the notes for Computer Science 51, a second
semester course in programming at Harvard College, which follows
the legendary CS50 course that introduces some half of all Harvard
undergraduate students to programming. The nature of the course –
introducing a wide range of programming abstractions and paradigms
with an eye toward developing a large design space of programs, using
functional programming as its base and OCaml as the delivery vehicle
– is shared with similar courses at a number of colleges. The instruc-
tors in those courses have for many years informally shared ideas,
examples, problems, and notes in an open and free-flowing manner.
When I took over the course from Greg Morrisett (now Dean of Com-
puting and Information Sciences at Cornell University), I became the
beneficiary of all of this collaboration, including source materials from
these courses – handouts, notes, lecture material, labs, and problem
sets – which have been influential in the structure and design of these
notes, and portions of which have thereby inevitably become inter-
mixed with my own contributions in a way that would be impossible
to disentangle. I owe a debt of gratitude to all of the faculty who have
been engaged in this informal sharing, especially,
1 Introduction 15
1.1 An extended example: greatest common divisor . . . . . 17
1.2 Programming as design . . . . . . . . . . . . . . . . . . . . 20
1.3 The OCaml programming language . . . . . . . . . . . . . 22
1.4 Tools and skills for design . . . . . . . . . . . . . . . . . . . 24
6 Functions 55
8
20 Concurrency 383
20.1 Sequential, concurrent, and parallel computation . . . . 384
20.2 Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . 385
20.3 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
20.4 Interthread communication . . . . . . . . . . . . . . . . . 389
20.5 Futures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392
20.6 Futures are not enough . . . . . . . . . . . . . . . . . . . . 394
20.7 Locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
20.7.1 Abstracting lock usage . . . . . . . . . . . . . . . . 399
20.8 Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
Bibliography 499
Index 503
He knew by heart the forms of the southern clouds at dawn on the 30th
of April, 1882, and could compare them in his memory with the mottled
streaks on a book in Spanish binding he had only seen once.. . . Two or
three times he had reconstructed a whole day; he never hesitated, but
each reconstruction had required a whole day. (Borges, 1962)
1 0.0000
and when the program is executed, it prints the table: 2 1.0000
3 1.5850
# printf "1 0.0000\ n"; 4 2.0000
# printf "2 1.0000\ n"; ⋯
# printf "3 1.5850\ n";
# printf "4 2.0000\ n" ;; Figure 1.2: A small table of logarithms
1 0.0000
2 1.0000
3 1.5850
4 2.0000
- : unit = ()
(For the moment, the details of the language in which this computa-
tion is written are immaterial. We’ll get to all that in a bit. The idea is
just to get the gist of the argument. In the meantime, you can just let
the code waft over you like a warm summer breeze.)
Now of course this code is hopelessly written. Why? Because it
treats each line of the table as an individual specimen, missing the
abstract view. The first step in viewing the lines abstractly is to note
INTRODUCTION 17
for each of several values of the variable x. (Again, the details of the
language being used are postponed, but you hopefully get the idea.)
This mechanism, the S TAT E VA R I A B L E , is thus a mechanism for ab-
straction – for making apparently dissimilar computations manifest an
underlying identity. To take full advantage of this type of variable, we’ll
need to specify the sequential values, 1 through 4 say, that the variable
takes on.
for x = 1 to 4 do
printf "%2d %2.4f\ n" x (log2 x)
done
This procedure works by counting down from the smaller of the two
numbers, one by one, until a common divisor is found. Since the
search for the common divisor is from the largest to the smallest possi-
bility, the greatest common divisor is found.
In the functional style, this same “countdown” algorithm might be
coded like this:
let gcd_func a b =
let rec downfrom guess =
if (a mod guess <> 0) || (b mod guess <> 0) then
downfrom (guess - 1)
else guess in
downfrom (min a b) ;;
fine, because the value of downfrom guess depends not on the value
of downfrom guess itself but of downfrom (guess - 1), a different
value. This may itself depend on downfrom (guess - 2), and so on,
but eventually one of the inputs to downfrom will be a common divisor,
and in that case, the output value of downfrom does not depend on
downfrom itself. The recursion “bottoms out” and the GCD is returned.
This style of programming – by defining and applying functions –
has a certain elegance, which can be seen already in the distinction
between the two versions of the GCD computation already provided.
But as it turns out, the algorithm underlying both of these implemen-
tations is a truly bad one. Counting down is just not the right way to
calculate the GCD of two numbers. As far back as 300 B C E , Euclid of
Alexandria provided a far better algorithm in Proposition 1 of Book
7 (Figure 1.4) of his treatise on mathematics, Elements. Euclid’s algo-
rithm for GCD is based on the following insight: Any square tiling of a
Figure 1.4: Proposition 1 of Book 7
20 by 28 area will tile both a 20 by 20 square and the 8 by 20 remainder. of Euclid’s Elements, providing his
More generally, any square tiling of an a by b area (where a is greater algorithm for calculating the greatest
common divisor of two numbers.
than b) will tile both a b by b square and the b by a − b remainder.
Thus, to calculate the GCD of a and b, it suffices to calculate the GCD
of b and a − b. Eventually, we’ll be looking for the GCD of two instances
of the same number (that is, a and b will be the same; we’ll be looking
to tile a square area) in which case we know the GCD; it is a (or b) it-
self. Figure 1.5 shows the succession of smaller and smaller rectangles
explored by Euclid’s algorithm for the 20 by 28 case.
An initial presentation of Euclid’s algorithm is this:
let rec gcd_euclid a b =
if a < b then gcd_euclid b a
else if a = b then a
else gcd_euclid b (a - b) ;;
28
28
8 8
4
20
20 20 4
4
(a) (b) (c) (d) (e)
Euclid’s algorithm for GCD shows us that there is more than one way
to solve a problem, and some ways are better than others. The dimen-
sions along which programmed solutions can be better or worse are
manifold. They include
• succinctness,
• efficiency,
• readability,
• maintainability,
• provability,
• testability,
• beauty.
adviser at Princeton.)
In this book, we concentrate on the following abstraction mecha-
nisms, listed with the style of programming they are associated with:
Table 1
Erlang
certainly don’t hurt. Erlang 115000 Scala
OCaml
OCaml
Scala 115000
Clojure
OCaml 114000 Go
1.4 Tools and skills for design
Clojure 110000 Groovy
Objective-C
Go 110000 F#
The space of design options available
Groovy to you is enabled
110000 by the palette Hack
Perl
of abstraction mechanisms thatObjective-C 110000
you can fluently deploy. Navigating Kotlin
F# 108000 Rust
the design space to find the best solutions is facilitated by a set of skills
108000
Swift
Hack
TypeScript
and analytic tools, which we will
Perlalso introduce throughout
106000 the follow- Bash/Shell
Kotlin 105000 CoffeeScript
ing chapters as they become pertinent. These include more precise ObjectPascal
Rust 105000
notions of the syntax and semantics of programming languages, fa- Haskell
Swift 102000 Java
cility with notations, sensitivityTypeScript
to programming style (see especially
102000
Lua
Ruby
Appendix B), programming interface
Bash/Shelldesign, unit testing,
100000 tools (big- Julia
CoffeeScript 100000 C
O notation, recurrence equations) for analyzing efficiency of code. JavaScript
ObjectPascal 100000
Python
Having these tools and skills at Haskell
your disposal will add to your computa-
100000 90K 95K 100K 105K 110K 115K 120K
tional tool-box and stretch yourJava
thinking about what it means to write
100000
Lua
good code. I expect, based on my own experiences,100000
that learning to Figure 1.8: United States average salary
Ruby 100000 by technology, from StackOverflow
develop, analyze, and express your software ideas with precision will Developer Survey 2018. Highlighted
Julia 98500
also benefit your abilities to develop,
C analyze, and express
98000 ideas more bars correspond to technologies in the
JavaScript 98000 typed functional family.
generally.
Python 98000
1
2
A Cook’s tour of OCaml
Upon running ocaml, you will see a P R O M P T (“#”) allowing you to type
an OCaml expression.
% ocaml
OCaml version 4.11.1
#
Exercise 1
The startup of the ocaml interpreter indicates that this is version 4.11.1 of the software.
What version of ocaml are you running?
Once the OCaml prompt is available, you can enter a series of
OCaml expressions to calculate the values that they specify. Numeric
(integer) expressions are a particularly simple case, so we’ll start with
those. The integer L I T E R A L S – like 3 or 42 or -100 – specify integer
values directly, but more complex expressions built by applying arith-
metic functions to other values do as well. Consequently, the OCaml
interpreter can be used as a kind of calculator.
# 42 ;;
- : int = 42
# 3 + 4 * 5 ;;
- : int = 23
# (3 + 4) * 5 ;;
- : int = 35
Since this is the first example we’ve seen of interaction with the
OCaml interpreter, some glossing may be useful. The OCaml interac-
tive prompt, ‘#’, indicates that the user can enter an OCaml expression,
such as ‘3 + 4 * 5’. A double semicolon ‘;;’ demarcates the end of
the expression. The system reads the expression, evaluates it (that
26 PROGRAMMING WELL
is, calculates its value), and prints an indication of the result, then
loops back to provide another prompt for the next expression. For
this reason, the OCaml interactive system is referred to as the “ R E A D -
2
E VA L - P R I N T L O O P ” or R E P L . Whenever we show the results of an 2
To exit the R E P L , just enter the end-
of-file character, ^d, typed by holding
interaction with the R E P L , the interpreter’s output will be shown in a
down the control key while pressing the
slanted font to distinguish it from the input. d key.
You’ll notice that the R E P L obeys the standard order of operations,
with multiplication before addition for instance. This precedence can
be overwritten in the normal manner using parentheses.
Exercise 2
Try entering some integer expressions into the OCaml interpreter and verify that appro-
priate values are returned.
Although we’ll introduce the aspects of the OCaml language in-
crementally over the next few chapters, to get a general idea of using
the language, we demonstrate its use with the GCD algorithm from
Chapter 1. We type the definition of the gcd_euclid function into the
R E P L:
Now we can make use of that definition to calculate the greatest com-
mon divisor of 20 and 28
# gcd_euclid 20 28 ;;
- : int = 4
The rules that form a noun phrase from an adjective and a noun
phrase or from a noun phrase and a noun are, respectively,
Here, we’ve rephrased the three rules as a single rule with three alter-
native right-hand sides. The BNF notation allows separating alterna-
tive right-hand sides with the vertical bar (∣) as we have done here.
A specification of a language using rules of this sort is referred to as
a G R A M M A R . According to this grammar, we can build noun phrases
like mad tea party
⟨nounphrase ⟩
⟨adjective ⟩ ⟨nounphrase ⟩
⟨noun ⟩ party
tea
⟨nounphrase ⟩
⟨nounphrase ⟩ ⟨noun ⟩
iced ⟨noun ⟩
tea
Notice the difference in structure. In mad tea party, the adjective mad
is combined with the phrase tea party, but in iced tea drinker, the
adjective iced does not combine with tea drinker. The drinker isn’t
iced; the tea is!
But these same rules can also be used to build an alternative tree for
“iced tea drinker”:
⟨nounphrase ⟩
⟨adjective ⟩ ⟨nounphrase ⟩
⟨noun ⟩ drinker
tea
The expression iced tea drinker is A M B I G U O U S (as is mad tea party);
the trees make clear the two syntactic analyses.
Importantly, as shown by these examples, it is the syntactic tree
structures that dictate what the expression means. The first tree seems
to describe a drinker of cold beverages, the second a cold drinker
of beverages. The syntactic structure of an utterance thus plays a
crucial role in its meaning. The characterization of the meanings of
expressions on the basis of their structure is the realm of S E M A N T I C S ,
pertinent to both natural and programming languages. We’ll come
back to the issue of semantics in detail in Chapters 13 and 19.
Exercise 3
Draw a second tree structure for the phrase mad tea party, thereby demonstrating that it
is also ambiguous.
Exercise 4
How many trees can you draw for the noun phrase flying purple people eater? Keep in
mind that flying and purple are adjectives and people and eater are nouns.
The English language, and all natural languages, are ambiguous that
way. Fortunately, context, intonation, and other clues disambiguate
30 PROGRAMMING WELL
Using these rules, we can build two trees for the expression 3 + 4 *
5:
⟨expr ⟩
⟨number ⟩ + ⟨number ⟩ 5
3 4
or
⟨expr ⟩
3 ⟨number ⟩ * ⟨number ⟩
4 5
E X P R E S S I O N S A N D T H E L I N G U I S T I C S O F P R O G R A M M I N G L A N G UA G E S 31
Exercise 5
What is the structure of the following OCaml expressions? Draw the corresponding
tree so that it reflects the actual precedences and associativities of OCaml. Then, try
typing the expressions into the R E P L to verify that they are interpreted according to the
structure you drew.
1. 10 / 5 / 2
2. 5. +. 4. ** 3. /. 2.
3. (5. +. 4.) ** (3. /. 2.)
4. 1 - 2 - 3 - 4
You may have been taught this kind of rule under the mnemonic
P E M D A S. But the ideas of precedence, associativity, and annotation
are quite a bit broader than the particulars of the P E M D A S convention.
They are useful in thinking more generally about the relationship
between what we will call concrete syntax and abstract syntax.
32 PROGRAMMING WELL
⟨expr ⟩
3 ⟨number ⟩ * ⟨number ⟩
4 5
We might abbreviate the tree structure to highlight the important
aspects by eliding the expression classes as
3 *
4 5
Then the alternative abstract syntax tree
+ 5
3 4
would correspond to the concrete syntax (3 + 4) * 5. Parentheses as
used for grouping are therefore notions of concrete syntax, not abstract
syntax. Similarly, conventions of precedence and associativity have to
do with the interpretation of concrete syntax, as opposed to abstract
syntax.
E X P R E S S I O N S A N D T H E L I N G U I S T I C S O F P R O G R A M M I N G L A N G UA G E S 33
In fact, there are multiple concrete syntax expressions for this ab-
stract syntax, such as (3 + 4) * 5, ((3 + 4) * 5), (3 + ((4))) *
5. But certain expressions that may seem related do not have this same
abstract syntax: 5 * (3 + 4) or ((4 + 3) * 5) or (3 + 4 + 0) *
5. Although these expressions specify the same value, they do so in
syntactically distinct ways. The fact that multiplication and addition
are commutative, or that 0 is an additive identity – these are semantic
properties, not syntactic.
Exercise 6
Draw the (abbreviated) abstract syntax tree for each of the following concrete syntax
expressions. Assume the further BNF rule
⟨expr ⟩ ∶∶= ⟨unop ⟩⟨expr ⟩
Exercise 7
What concrete syntax corresponds to the following abstract syntax trees? Show as many
as you’d like.
1. ~-
1 42
2. /
84 +
0 42
3. +
84 /
0 42
Edict of intention:
Make your intentions clear.
Programmers make mistakes. If their intentions are well expressed,
other programmers reviewing the code can notice that those inten-
tions are inconsistent with the code. Even the computer interpreting
the program can itself take appropriate action, notifying the program-
mer with a useful error or warning before the code is executed and the
unintended behavior can manifest itself.
Over the next chapters, we’ll see many ways that the edict of inten-
tion is applied. One of the most fundamental is through documenta-
tion of code.
3.4.1 Commenting
One of the most valuable aspects of the concrete syntax of any pro-
gramming language is the facility to provide elements in a concrete
program that have no correspondence whatsoever in the abstract syn-
tax, and therefore no effect on the computation expressed by the pro-
gram. The audience for such C O M M E N T S is the population of human
readers of the program. Comments serve the crucial expressive pur-
pose of documenting the intended workings of a program for those
human readers.
In OCaml, comments are marked by surrounding them with special
delimiters: (* ⟨⟩ *).4 The primary purpose of comments is satisfying 4
We use the symbol ⟨⟩ here and
throughout the later chapters as a con-
the edict of intention. Comments should therefore describe the why
venient notation to indicate unspecified
rather than the how of a program. Section B.2 presents some useful text of some sort, a textual anonymous
stylistic considerations in providing comments for documenting pro- variable of a sort. Here, it stands in
for the text that forms the comment.
grams. In other contexts it stands in for the
There are other aspects of concrete syntax that can be freely de- arguments of an operator, constructor,
or subexpression, for instance, in ⟨⟩ + ⟨⟩
ployed because they have no affect on the computation that a program
or ⟨⟩ list or let ⟨⟩ in ⟨⟩ .
E X P R E S S I O N S A N D T H E L I N G U I S T I C S O F P R O G R A M M I N G L A N G UA G E S 35
carries out. These too can be judiciously deployed to help express your
intentions. For instance, the particular spacing used in laying out the
elements of a program doesn’t affect the computation that the program
expresses. Spaces, newlines, and indentations can therefore be used to
make your intentions clearer to a reader of the code, by laying out the
code in a way that emphasizes its structure or internal patterns. Simi-
larly, the choice of variable names is completely up to the programmer.
Names can be consistently renamed without affecting the computa-
tion. Programmers can take advantage of this fact by choosing names
that make clear their intended use.
OCaml is a
• value-based,
• functional
The OCaml language is, at its heart, a language for calculating values.
The expressions of the language specify these values, and the process
of calculating the value of an expression is termed E VA L U AT I O N . We’ve
already seen examples of OCaml evaluating some simple expressions
in Chapter 2:
# 3 + 4 * 5 ;;
- : int = 23
# (3 + 4) * 5 ;;
- : int = 35
The results of these evaluations are integers, and the output printed by
the R E P L indicates this by the int, about which we’ll have more to say
shortly.
Notice that the floating point operators are distinct from those for
integers. Though this will take some getting used to, the reason for this
design decision in the language will become apparent shortly.
VA L U E S A N D T Y P E S 39
Exercise 8
Use the OCaml R E P L to calculate the value of the G O L D E N R AT I O , a proportion thought
to be especially pleasing to the eye (Figure 4.1).
√
1+ 5
.
2
You’ll want to use the built in sqrt function for floating point numbers. Be careful to use
floating point literals and operators. If you find yourself confronted with errors in solving
this exercise, come back to it after reading Section 4.2.
As in many programming languages, text is represented as strings Figure 4.1: A rectangle with width and
of C H A R A C T E R S . Character literals are given in single quotes, for in- height in the golden ratio.
stance, ’a’, ’X’, ’3’. Certain special characters can be specified only
by escaping them with a backslash, for instance, the single-quote char-
acter itself ’\’’ and the backslash ’\\’, as well as certain whitespace
characters like newline ’\n’ or tab ’\t’.
String literals are given in double quotes (with special characters
similarly escaped), for instance, "", "first", " and second". They
can be concatenated with the ^ operator.2 2
A useful trick is to use the escape
sequence of a backslash, a newline, and
# "" ^ "first" ^ " and second" ;; any amount of whitespace, all of which
- : string = "first and second" will be ignored, so as to split a string
over multiple lines. For instance,
# "First, " ^ "second, \
4.1.4 Truth values and expressions # third, \
# and fourth." ;;
There are two T RU T H VA L U E S , indicated in OCaml by the literals true - : string = "First, second, third, and fourth."
and false. Logical reasoning based on truth values was codified by the
British mathematician George Boole (1815–1864), leading to the use of
the term boolean for such values, and the type name bool for them in
OCaml.
Just as arithmetic values can be operated on with arithmetic oper-
ators, the truth values can be operated on with logical operators, such
as operators for conjunction (&&), disjunction (||), and negation (not).
(See Section A.2 for definitions of these operators.)
# false ;;
- : bool = false
# true || false ;;
- : bool = true
# true && false ;;
- : bool = false
# true && not false ;;
- : bool = true
# 3 = 3 ;;
- : bool = true
# 3 > 4 ;;
- : bool = false
# 1 + 1 = 2 ;;
- : bool = true
# 3.1416 = 314.16 /. 100. ;;
- : bool = false
# true = false ;;
- : bool = false
# true = not false ;;
- : bool = true
# false < true ;;
- : bool = true
Exercise 9
Are any of the results of these comparisons surprising? See if you can figure out why the
results are that way.
Of course, the paradigmatic use of truth values is in the ability
to compute different values depending on the truth or falsity of a
condition. The OCaml C O N D I T I O N A L expression follows the template4 4
We describe the syntax of the construct
using the angle bracket convention
if ⟨exprtest ⟩ then ⟨exprtrue ⟩ else ⟨exprfalse ⟩ for classes of expression used in BNF
rules as introduced in Chapter 3. We
whose effect is to return the value of the ⟨exprtrue ⟩ if the value of the will continue to do so throughout as
test expression ⟨exprtest ⟩ is true and the value of the ⟨exprfalse ⟩ if the we introduce new constructs of the
language.
value of ⟨exprtest ⟩ is false. As mentioned in footnote 2 on
# if 3 = 3 then 0 else 1 ;; page 30, in defining expression classes
using this notation, we use subscripts
- : int = 0
to differentiate among different occur-
# 2 * if 3 > 4 then 3 else 4 + 5 ;;
rences of the same expression class,
- : int = 18
as we have done here with the three
# 2 * (if 3 > 4 then 3 else 4) + 5 ;; instances of the ⟨expr ⟩ class – ⟨exprtest ⟩,
- : int = 13 ⟨exprtrue ⟩, and ⟨exprfalse ⟩.
is a frequent source of bugs in code, even if not with the dramatic after-
math of the Ariane 5 explosion. As we will see, associating values with
types can often prevent these kinds of bugs.
The OCaml language is S TAT I C A L LY T Y P E D , in that the type of an
expression can be determined just by examining the expression in
its context. It is not necessary to run the code in which an expression
occurs in order to determine the type of an expression, as might be
necessary in a DY N A M I C A L LY T Y P E D language (Python or JavaScript,
for instance).
Types are themselves a powerful abstraction mechanism. Types are
essentially abstract values. By reasoning about the types of expressions,
we can convince ourselves of the correctness of code without having to
run it.
Furthermore, OCaml is S T R O N G LY T Y P E D ; values may not be
used in ways inappropriate for their type. One of the ramifications of
OCaml’s strong typing is that functions only apply to values of certain
types and only return values of certain types. For instance, the addition
function specified by the + operator expects integer arguments and
returns an integer result.
By virtue of strong, static typing, the programming system (com-
piler or interpreter) can tell the programmer when type constraints are
violated even before the program is run, thereby preventing bugs before
they happen. If you attempt to use a value in a manner inconsistent
with its type, OCaml will complain with a typing error. For instance,
integer multiplication can’t be performed on floating point numbers or
strings:
# 5 * 3 ;;
- : int = 15
# 5 * 3.1416 ;;
Line 1, characters 4-10:
1 | 5 * 3.1416 ;;
^^^^^^
Error: This expression has type float but an expression was
expected of type
int
# "five" * 3 ;;
Line 1, characters 0-6:
1 | "five" * 3 ;;
^^^^^^
Error: This expression has type string but an expression was
expected of type
int
Programmers using a language with strong static typing for the first
time often find the frequent type errors limiting and even annoying.
Furthermore, there are some computations that can’t be expressed well
with such strict limitations, especially low-level systems computations
42 PROGRAMMING WELL
integers int 1 -2 42 (3 + 4) * 5
floating point numbers float 3.14 -2. 2e12 (3.0 +. 4.) *. 5e0
characters char ’a’ ’&’ '\n' char_of_int (int_of_char ’s’)
strings string "a" "3 + 4" "re" ^ "bus"
truth values bool true false true && not false
unit unit () ignore (3 + 4)
rors could show up at run time after the code has been deployed – and
when it’s far too late to repair it. Strong static type constraints are thus
an example of a language restraint that frees programmers from verify-
ing that their code does not contain “bad” operations by empowering
the language interpreter to do so itself. (Looking ahead to the edict of
prevention in Chapter 11, it makes the illegal inexpressible.)
# 42 ;;
- : int = 42
# 3.1416 ;;
- : float = 3.1416
# false ;;
- : bool = false
Notice that the R E P L presents the type of each computed value after a
colon (:). (Why a colon? You’ll see shortly.)
Table 4.1 provides a more complete list of some of the atomic types
in OCaml (some not yet introduced), along with their type names,
some example values, and an example expression that specifies a value
of that type using some functions that return values of the given type.
(We’ll get to non-atomic (composite) types in Chapter 7.)
It is often useful to notate that a certain expression is of a certain
VA L U E S A N D T Y P E S 43
• 42 : int
• true : bool
• 3.14 *. 2. *. 2. : float
The first states that the expression 42 specifies an integer value, the
second that true specifies a boolean truth value, and so forth. The :
operator is sometimes read as “the”, thus “42, the integer” or “true, the
bool”. The typing operator is special in that it combines an expression
from the value language (to its left) with an expression from the type
language (to its right).
We can test out these typings right in the R E P L . (The parentheses
are necessary.)
# (42 : int) ;;
- : int = 42
# (true : bool) ;;
- : bool = true
# (3.14 *. 2. *. 2. : float) ;;
- : float = 12.56
# (if 3 > 4 then 3 else 4 : int) ;;
- : int = 4
# (42 : float) ;;
Line 1, characters 1-3:
1 | (42 : float) ;;
^^
Error: This expression has type int but an expression was expected
of type
float
Hint: Did you mean `42.'?
Exercise 10
Which of the following typings hold?
1. 3 + 5 : float
2. 3. + 5. : float
3. 3. +. 5. : float
4. 3 : bool
5. 3 || 5 : bool
6. 3 || 5 : int
44 PROGRAMMING WELL
Try typing these into the R E P L to see what happens. (Remember to surround them with
parentheses.)
Finally, in OCaml, expressions are I M P L I C I T LY T Y P E D . Although all
expressions have types, and the types of expressions can be annotated
using typings, the programmer doesn’t need to specify those types in
general. Rather, the OCaml interpreter can typically deduce the types
of expressions at compile time using a process called T Y P E I N F E R -
E N C E. In fact, the examples shown so far depict this inference. The
REPL prints not only the value calculated for each expression but also
the type that it inferred for the expression.
Exercise 11
Give a typing for a value of the unit type.
as well. In Exercise 8, you used the sqrt function to take the square
root of a floating point number. This function, sqrt, is itself a value
and has a type. The type of a function expresses both the type of its
argument (in this case, float) and the type of its output (again float).
The type expression for a function (the type’s “name”) is formed by
placing the symbol -> (read “arrow” or “to”) between the argument
type and the output type. Thus the type for sqrt is float -> float
(read “float arrow float” or “float to float”), or, expressed as a typing,
sqrt : float -> float.
You can verify this typing yourself, just by evaluating sqrt:
# sqrt ;;
- : float -> float = <fun>
Since functions are themselves values, they can be evaluated, and the
REPL performs type inference and provides the type float -> float
along with a printed representation of the value itself <fun>, indicating
that the value is a function of some sort.5 5
The actual value of a function is a
complex data object whose internal
Because the argument type of sqrt is float, it can only be applied
structure is not useful to print, so this
to values of that type. And since the result type of sqrt is float, only abstract presentation <fun> is printed
functions that take float arguments can apply to expressions like instead.
sqrt 42..
Exercise 12
Try applying the sqrt function to an argument of some type other than float, for
instance, a value of type bool. What happens?
Of course, the real power in functional programming comes from
defining your own functions. We’ll move to this central topic in Chap-
ter 6, but first, it is useful to provide a means of naming values (includ-
ing functions), to which we turn in the next chapter.
5
Naming and scope
the variable pi is of type float. What else could it be, given that its
value is a float literal, and it is used as an argument of the *. oper-
ator, which takes float arguments? You would be right, and OCaml
itself can make this determination, inferring the type of pi without the
explicit typing being present. For that reason, the type information in
the let construct is optional. We can simply write
let pi = 3.1416 in
pi *. 2. *. 2. ;;
and the calculation proceeds as usual. This ability to infer types is what
we mean when we say (as in Section 4.2.1) that OCaml is implicitly
typed.
Although these typings when introducing variables are optional,
nonetheless, it can still be useful to provide explicit type information
when naming a value. First, following the edict of intention, it allows
the programmer to make clear the intended types, so that the OCaml
interpreter can verify that the programmer’s intention was followed
and so that readers of the code are aware of that intention. Second,
there are certain (relatively rare) cases (Section 9.7) in which OCaml
cannot infer a type for an expression in context; in such cases, the
explicit typing is necessary.
Remember that all expressions in OCaml have values, even let expres-
sions. Thus we can use them as subexpressions of larger expressions.
Exercise 13
Are the parentheses necessary in this example? Try out the expression without the
parentheses and see what happens.
A particularly useful application of the fact that let expressions
can be used as first-class values is that they may be embedded in other
let expressions to get the effect of defining multiple names. Here, we
define both the constant π and a radius to calculate the area of a circle
of radius 4:
# let pi = 3.1416 in
# let radius = 4. in
# pi *. radius ** 2. ;;
- : float = 50.2656
NAMING AND SCOPE 49
Exercise 14
Use the let construct to improve the readability of the following code to calculate the
length of the hypotenuse of a particular right triangle:
# sqrt (1.88496 *. 1.88496 +. 2.51328 *. 2.51328) ;;
- : float = 3.1416
5.4 Scope
The name defined in the let expression is available only in the body
of the expression. The name is L O C A L to the body, and unavailable
outside of the body. We say that the S C O P E of the variable – that is, the
code within which the variable is available as a name of the defined
value – is the body of the let expression. This explains the following
behavior:
# (let s = "hi ho " in
# s ^ s) ^ s ;;
Line 2, characters 9-10:
2 | s ^ s) ^ s ;;
^
Error: Unbound value s
The body of the let expression in this example ends at the closing
parenthesis, and thus the variable s defined by that construct is un-
available (“unbound”) thereafter.
Exercise 15
Correct the example to provide the triple concatenation of the defined string.
Exercise 16
What type do you expect is inferred for s in the example?
In particular, the scope of a local let naming does not include the
definition itself (the ⟨exprdef ⟩ part between the = and the in). Thus the
following expression is ill-formed:
# let x = x + 1 in
# x * 2 ;;
Line 1, characters 8-9:
1 | let x = x + 1 in
^
Error: Unbound value x
And a good thing too, for what would such an expression mean? This
kind of recursive definition isn’t well founded. Nonetheless, there are
useful recursive definitions, as we will see in Section 6.4.
What if we define the same name twice? There are several cases to
consider. Perhaps the two uses are disjoint, as in this example:
# sqrt ((let x = 3. in x *. x)
# +. (let x = 4. in x *. x)) ;;
- : float = 5.
50 PROGRAMMING WELL
Since each x is introduced with its own let and has its own body, the
scopes are disjoint. The occurrences of x in the first expression name
the number 3. and in the second name the number 4.. But in the
following case, the scopes are not disjoint:
# sqrt (let x = 3. in
# x *. x +. (let x = 4. in x *. x )) ;;
- : float = 5.
The scope of the first let encompasses the entire second let. Do the
highlighted occurrences of x in the body of the second let name 3.
or 4.? The rule used in OCaml (and most modern languages) is that
the occurrences are bound by the nearest enclosing binding construct
for the variable. The same binding relations hold as if the inner let-
bound variable x and the occurrences of x in its body were uniformly
renamed, for instance, as y:
# sqrt (let x = 3. in
# x *. x +. (let y = 4. in y *. y)) ;;
- : float = 5.
the innermost x (naming 4) shadows the outer two, and the middle x
(naming 2) shadows the outer x (naming 1). Thus the three highlighted
occurrences of x name 1, 2, and 4, respectively, which the expression as
a whole sums to 7.
Since the scope of a let-bound variable is the body of the construct,
but not the definition, occurrences of the same variable in the defi-
nition must be bound outside of the let. Consider the highlighted
occurrence of x on the second line:
let x = 3 in
let x = x * 2 in
x + 1 ;;
This occurrence is bound by the let in line 1, not the one in line 2.
That is, it is equivalent to the renaming
let x = 3 in
let y = x * 2 in
y + 1 ;;
NAMING AND SCOPE 51
Exercise 17
For each occurrence of the variable x in the following examples, which let construct
binds it? Rewrite the expressions by renaming the variables to make them distinct while
preserving the bindings.
1. let x = 3 in
let x = 4 in
x * x ;;
2. let x = 3 in
let x = x + 2 in
x * x ;;
3. let x = 3 in
let x = 4 + (let x = 5 in x) + x in
x * x ;;
# let pi = 3.1416 ;;
val pi : float = 3.1416
# let radius = 4.0 ;;
val radius : float = 4.
# pi *. radius *. radius ;;
- : float = 50.2656
# 2. *. pi *. radius ;;
- : float = 25.1328
# let x = 3 ;;
val x : int = 3
# let x = x + 1 ;;
val x : int = 4
# x + x ;;
- : int = 8
The second line may look like it is assigning a new value to x. But no,
all that is happening is that there is a new name (coincidentally the
same as a previous name) for a new value. The old name x for the value
3 is still around; it’s just inaccessible, shadowed by the new name x. (In
Chapter 15, we provide a demonstration that this is so.)
52 PROGRAMMING WELL
Exercise 18
In the sequence of expressions
let tax_rate = 0.05 ;;
let price = 5. ;;
let price = price * (1. +. tax_rate) ;;
price ;;
what is the value of the final expression? (You can use the R E P L to verify your answer.)
Global naming is available only at the top level. A global name
cannot be defined from within another expression, for instance, the
body of a local let. The following is thus not well-formed:
# let radius = 4. in
# let pi = 3.1416 in
# let area = pi *. radius ** 2. ;;
Line 3, characters 30-32:
3 | let area = pi *. radius ** 2. ;;
^^
Error: Syntax error
Exercise 19
How might you get the effect of this definition of a global variable area by making use of
local variables for pi and radius?
s
6
Functions
-2 → -1
-1 → 0
0→1
1→2
2→3
⋮
-2 → true
-1 → false
0 → true
1 → false
2 → true
⋮
calculus. (See Section 1.2.) In the lambda calculus, functions and their
application are so central (indeed, there’s basically nothing else in the
logic) that the addition of the parentheses in the function application
notation becomes onerous. Instead, Church proposed merely prefix-
ing the function to its argument. Instead of f (1), Church’s notation
would have f 1. Instead of f (g (1)), he would have f (g 1), using the
parentheses for grouping, but not for demarcating the arguments.
Similarly, in OCaml, the function merely precedes its argument. The
successor of 41 is simply succ 41. The square root of two is sqrt 2.0.
# succ 41 ;;
- : int = 42
# sqrt 2.0 ;;
- : float = 1.41421356237309515
Recall from Section 4.4 that functions (as all values) have types,
which can be expressed as type expressions using the -> operator.
For instance, the successor function has the type given by the type Figure 6.1: Moses Schönfinkel (1889–
expression int -> int and the “evenness” function the type int -> 1942), Russian logician and math-
ematician, first specified the use of
bool. higher-order functions to mimic the
effect of multiple-argument functions.
In essence, the function takes its three arguments one at a time, return-
ing a function after each argument before the last. Although this trick Figure 6.2: Haskell Curry (1900–1982),
American logician, promulgator of
was first discussed by Schönfinkel (1924), it is referred to as C U R RY I N G the use of higher-order functions
a function, the resulting function being curried, so named after Haskell to simulate functions of multiple
arguments, which is referred to as
Curry who popularized the approach. currying in his honor.
Because in OCaml functions take one argument, the language
makes extensive use of currying, and language constructs facilitate
its use. For instance, the -> type expression operator is right associa-
tive (see Section 3.2) in OCaml, so that the type of the curried three-
argument function above can be expressed as
FUNCTIONS 57
# (+) ;;
- : int -> int -> int = <fun>
# (/.) ;;
- : float -> float -> float = <fun>
# (&&) ;;
- : bool -> bool -> bool = <fun>
Exercise 20
What (if anything) are the types and values of the following expressions? Try to figure
them out yourself before typing them into the R E P L to verify your answer.
1. (-) 5 3
2. 5 - 3
3. - 5 3
4. "O" ^ "Caml"
5. (^) "O" "Caml"
6. (^) "O"
7. ( ** ) – See footnote 3.
The arrow -> separates the typing of a variable that represents the
input, the integer x, from an expression that represents the output, 2
* x. The output expression can, of course, make free use of the input
variable as part of the computation.
We can apply this function to an argument (21, say). We use the
usual OCaml prefix function application syntax, placing the function
before its argument:
# (fun x : int -> 2 * x) 21 ;;
- : int = 42
Exercise 21
Try defining your own functions, perhaps one that squares a floating point number, or
one that repeats a string.
Now that we have the ability to define functions (with fun) and the
ability to name values (with let), we can put them together to name
newly-defined functions. Here, we give a global naming of the dou-
bling function and use it:
# let double = fun x -> 2 * x ;;
val double : int -> int = <fun>
# double 21 ;;
- : int = 42
Here are functions for the circumference and area of circles of given
radius:
FUNCTIONS 59
# let pi = 3.1416 ;;
val pi : float = 3.1416
# let area =
# fun radius ->
# pi *. radius ** 2. ;;
val area : float -> float = <fun>
# let circumference =
# fun radius ->
# 2. *. pi *. radius ;;
val circumference : float -> float = <fun>
# area 4. ;;
- : float = 50.2656
# circumference 4. ;;
- : float = 25.1328
This syntax for defining functions may be more familiar from other
languages. It is also consistent with a more general pattern-matching
syntax that we will come to in Section 7.2.
This compact syntax for function definition is an example of S Y N -
5
TA C T I C S U G A R , a bit of additional syntax that serves to abbreviate 5
The term “syntactic sugar” was first
used by Landin (1964) (Figure 17.7)
a more complex construction. By adding some syntactic sugar, the
to describe just such abbreviatory
language can provide simpler expressions without adding underlying constructs.
constructs to the language; a language with a small core set of con-
structs can still have a sufficiently expressive concrete syntax that it
is pleasant to program in. As we introduce additional syntactic sugar
constructs, notice how they allow for idiomatic programming without
increasing the core language.
We can use this more compact function definition notation to
provide a more elegant definition of the doubling function:
# let double x = 2 * x ;;
val double : int -> int = <fun>
# double (double 3) ;;
- : int = 12
60 PROGRAMMING WELL
is syntactic sugar for (and hence completely equivalent to) the defini-
tion
# let hypotenuse =
# fun x ->
# fun y ->
# sqrt (x ** 2. +. y ** 2.) ;;
val hypotenuse : float -> float -> float = <fun>
As in all definitions, you can provide a typing for the variable being
defined, as in
# let hypotenuse : float -> float -> float =
# fun x ->
# fun y ->
# sqrt (x ** 2. +. y ** 2.) ;;
val hypotenuse : float -> float -> float = <fun>
Here, we have recorded that x and y are each of float type, and the re-
sult of an application hypotenuse x y is also a float, which together
FUNCTIONS 61
capture the full information about the type of hypotenuse itself. Con-
sequently, the type inferred for the hypotenuse function itself is, as
before, float -> float -> float, that is, a curried binary function
from floats to floats.
Exercise 22
Consider the following beginnings of function declarations. How would these appear
using the compact notation?
3. let foo : (float -> int) -> float -> bool = ...
Exercise 23
What are the types for the following expressions?
Exercise 24
Define a function square that squares a floating point number. For instance,
# square 3.14 ;;
- : float = 9.8596
# square 1234567. ;;
- : float = 1524155677489.
Exercise 25
Define a function abs : int -> int that computes the absolute value of an integer.
# abs (-5) ;;
- : int = 5
# abs 0 ;;
- : int = 0
# abs (3 + 4) ;;
- : int = 7
Exercise 26
The Stdlib.string_of_bool function returns a string representation of a boolean.
Here it is in operation:
# string_of_bool (3 = 3) ;;
- : string = "true"
# string_of_bool (0 = 3) ;;
- : string = "false"
What is the type of string_of_bool? Provide your own function definition for it.
Exercise 27
Define a function even : int -> bool that determines whether its integer argument
is an even number. It should return true if so, and false otherwise. Try using both the
compact notation for the definition and the full desugared notation. Try versions with
and without typing information for the function name.
Exercise 28
Define a function circle_area : float -> float that returns the area of a circle of a
given radius specified by its argument. Try all of the variants described in Exercise 27.
Exercise 29
A frustrum (Figure 6.3) is a three-dimensional solid formed by slicing off the top of a
cone parallel to its base. The volume V of a frustrum with radii r 1 and r 2 and height h is
given by the formula
πh
V = (r 12 + r 1 r 2 + r 22 ) .
3
Implement a function to calculate the volume of a frustrum given the radii and height.
Problem 30
The calculation of the date of Easter, a calculation so important to early Christianity
that it was referred to simply as C O M P U T U S (“the computation”), has been the subject
of innumerable algorithms since the early history of the Christian church. An especially
simple method, published in Nature in 1876 and attributed to “A New York correspon-
dent” (1876), proceeds by sequentially calculating the following values on the basis of the
year Y :
n ! = n ⋅ (n − 1)!
The body of the function starts by distinguishing the two cases, when n
is zero and when n is positive.
let fact (n : int) : int =
if n = 0 then ...
else ...
There seems to be a problem. Recall from Section 5.4 that the scope
of a let is the body of the let (or the code following a global let),
but not the definition part of the let. Yet we’ve referred to the name
fact in the definition of the fact function. The scope rules for the let
constructs (both local and global) disallow this.
In order to extend the scope of the naming to the definition itself, to
allow a recursive definition, we add the rec keyword after the let.
# let rec fact (n : int) : int =
# if n = 0 then 1
# else n * fact (n - 1) ;;
val fact : int -> int = <fun>
The rec keyword means that the scope of the let includes not only its
body but also its definition part. With this change, the definition goes
through, and in fact, the function works well:
64 PROGRAMMING WELL
# fact 0 ;;
- : int = 1
# fact 1 ;;
- : int = 1
# fact 4 ;;
- : int = 24
# fact 20 ;;
- : int = 2432902008176640000
You may in the past have been admonished against defining some-
thing in terms of itself, such as “comb: an object used to comb one’s
hair; to comb: to run a comb through.” You may therefore find some-
thing mysterious about recursive definitions. How can we make use of
a function in its own definition? We seem to be using it before it’s even
fully defined. Isn’t that problematic?
Of course, recursive definition can be problematic. For instance, 7
In fact, the computer scientist C. A. R.
Hoare in his 1981 Turing Award lecture
consider this recursive definition of a function to add “just one more”
described his own introduction to
to a recursive invocation of itself: recursion this way:
But a recursion that is well founded can be quite useful.7 In the case as a computer scientist is
founded. Due credit must
of factorial, each recursive invocation of fact is given an argument
be paid to the genius of the
that is one smaller than the previous invocation, so that eventually designers of A L G O L 60 who
an invocation on argument 0 will occur and the recursion will end. included recursion in their
Because there are branches of computation (namely, the first arm language and enabled me
of the conditional) without recursive invocations of fact, and those to describe my invention
branches will eventually be taken, all is well. so elegantly to the world.
I have regarded it as the
But will those branches always be eventually taken? Unfortunately
highest goal of programming
not. language design to enable
# fact (~-5) ;;
good ideas to be elegantly
Stack overflow during evaluation (looping recursion?). expressed. (Hoare, 1981)
This looks familiar. Counting down from any non-negative integer will
eventually get us to zero. But counting down from a negative integer
won’t. We intended the factorial function to apply only to non-negative
FUNCTIONS 65
integers, the values for which it’s defined, but we didn’t express that
intention – the edict of intention again – with this unfortunate result.
You might think that we could solve this problem with types. In-
stead of specifying the argument as having integer type, perhaps we
could specify it as of non-negative integer type. Unfortunately, OCaml
does not provide for this more fine-grained type, and in any case, other
examples might require different constraints on the type, perhaps odd
integers only, or integers larger than 7, or integers within a certain
range.
Exercise 31
For each of the following cases, define a recursive function of a single argument for
which the definition is well founded (and the computation terminates) only when the
argument is
8
1. an odd integer; If you are interested in the issue, you
might explore the literature on so-called
2. an integer less than or equal to 5;
D E P E N D E N T T Y P E S Y S T E M S , which
3. the integer 0; provide this expanded expressivity at
4. the truth value true. the cost of much more complex type
inference computations.
OCaml’s type system isn’t expressive enough to capture these fine-
grained distinctions.8 Instead, we’ll have to deal with such anomalous
conditions using different techniques, which will be the subject of
Chapter 10.
Exercise 32
Imagine tiling a floor with square tiles of ever-increasing size, each one abutting the
previous two, as in Figure 6.4. The sides of the tiles grow according to the F I B O N A C C I
S E QU E N C E , in which each number is the sum of the previous two. By convention, the
first two numbers in the sequence are 0 and 1. Thus, the third number in the sequence is
0 + 1 = 1, the fourth is 1 + 1 = 2, and so forth.
The first 10 numbers in the Fibonacci sequence are Figure 6.4: A Fibonacci tiling.
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . .
The Fibonacci sequence has connections to many natural phenomena, from the
spiral structure of seashells (as alluded to in the figure) to the arrangement of seeds in a
sunflower to the growth rate of rabbits. It even relates to the golden ratio: the tiled area
depicted in the figure tends toward a golden rectangle (see Exercise 8) as more tiles are
added. (Exercise 187 explores this fact.)
Define a recursive function fib : integer -> integer that given an index into the
Fibonacci sequence returns the integer at that index. For instance,
# fib 1 ;;
- : int = 0
# fib 2 ;;
- : int = 1
# fib 8 ;;
- : int = 13
Exercise 33
Define a function fewer_divisors : int -> int -> bool, which takes two integers,
n and bound, and returns true if n has fewer than bound divisors (including 1 and n). For
example:
# fewer_divisors 17 3 ;;
- : bool = true
# fewer_divisors 4 3 ;;
- : bool = false
# fewer_divisors 4 4 ;;
- : bool = true
66 PROGRAMMING WELL
Do not worry about zero or negative arguments or divisors. Hint: You may find it useful
to define an auxiliary function to simplify the definition of fewer_divisors.
# fact 1 ;;
- : int = 1
# fact 2 ;;
- : int = 2
# fact 5 ;;
- : int = 120
# fact 10 ;;
- : int = 3628800
# fact 1 = 1 ;;
- : bool = true
# fact 2 = 2 ;;
- : bool = true
# fact 5 = 120 ;;
- : bool = true
# fact 10 = 3628800 ;;
- : bool = true
A unit testing function for fact, call it fact_test, verifies that fact
calculates the correct values for representative examples. (Let’s start
with these.) One approach is to simply evaluate each of the conditions
and make sure that they are all true.
FUNCTIONS 67
# let fact_test () =
# fact 1 = 1
# && fact 2 = 2
# && fact 5 = 120
# && fact 10 = 3628800 ;;
val fact_test : unit -> bool = <fun>
If all of the tests pass (as they do in this case), the testing function
returns true. If any test fails, it returns false. Unfortunately, in the
latter case it provides no help in tracking down the tests that fail.
In order to provide information about which tests have failed, we’ll
print an indicative message associated with the test. An auxiliary
function to handle the printing will be helpful:9 9
We’re making use here of two language
constructs that, strictly speaking, belong
# let unit_test (test : bool) (msg : string) : unit = in later chapters, as they involve side
# if test then effects, computational artifacts that
# Printf.printf "%s passed\ n" msg don’t affect the value expressed: the
# else sequencing operator (;) discussed in
# Printf.printf "%s FAILED\ n" msg ;; Section 15.3, and the printf function in
the Printf library module. Side effects
val unit_test : bool -> string -> unit = <fun>
in general are introduced in Chapter 15.
Now the fact_test function can call unit_test to verify each of the
conditions.
# let fact_test () =
# unit_test (fact 1 = 1) "fact 1";
# unit_test (fact 2 = 2) "fact 2";
# unit_test (fact 5 = 120) "fact 5";
# unit_test (fact 10 = 3628800) "fact 10" ;;
val fact_test : unit -> unit = <fun>
# let hypotenuse_test () =
# unit_test (hypotenuse 0. 0. = 0.) "hyp 0 0";
# unit_test (hypotenuse 1. 1. = 1.41421356) "hyp 1 1" ;;
val hypotenuse_test : unit -> unit = <fun>
# hypotenuse_test () ;;
hyp 0 0 passed
hyp 1 1 FAILED
- : unit = ()
The test reveals a problem. The unit triangle test has failed, not
because the hypotenuse function is wrong but because the value we’ve
proposed isn’t exactly the floating point number calculated. The float
type has a fixed capacity for representing numbers, and can’t therefore
represent all numbers exactly. The best we can do is check that floating
point calculations are approximately correct, within some tolerance.
Rather than checking the condition as above, instead we can check
that the value is within, say, 0.0001 of the value in the test, a condition
like this:
# let hypotenuse_test () =
# unit_test_within 0.0001 (hypotenuse 0. 0.) 0. "hyp 0 0";
# unit_test_within 0.0001 (hypotenuse 1. 1.) 1.4142 "hyp 1 1";
# unit_test_within 0.0001 (hypotenuse ~-.1. 1.) 1.4142 "hyp -1 1";
# unit_test_within 0.0001 (hypotenuse 2. 2.) 2.8284 "hyp 2 2" ;;
val hypotenuse_test : unit -> unit = <fun>
# hypotenuse_test () ;;
hyp 0 0 passed
hyp 1 1 passed
hyp -1 1 passed
hyp 2 2 passed
- : unit = ()
We’ll return to the question of unit testing in Sections 10.5 and 17.6,
when we have more advanced tools to use.
7
Structured data and composite types
The kinds of data that we’ve introduced so far have been unstructured.
The values are separate atoms,1 discrete undecomposable units. Each 1
The term “atom” is used here in its
sense from Democritus and other clas-
integer is separate and atomic, each floating point number, each truth
sical philosophers, the indivisible units
value. But the power of data comes from the ability to build new data making up the physical universe. Now,
from old by putting together data structures. of course, we know that though chemi-
cal elements are made of atoms, those
In this chapter, we’ll introduce three quite general ways built into atoms themselves have substructure
OCaml to structure data: tuples, lists, and records. For each such way, and are not indivisible. Unlike the phys-
ical world, the world of discrete data can
we describe how to construct structures from their parts using value
be well thought of as being built from
constructors; what the associated type of the structures is and how to indivisible atoms.
construct a type expression for them using type constructors; and how
to decompose the structures into their component parts using pattern-
matching. (We turn to methods for generating your own composite
data structures in Chapter 11.) We start with tuples.
7.1 Tuples
Exercise 34
What are the types for the following pair expressions?
1. false, 5
2. false, true
3. 3, 5
4. 3.0, 5
5. 5.0, 3
6. 5, 3
7. succ, pred
Exercise 35
Construct a value for each of the following types.
1. bool * bool
2. bool * int * float
3. (bool * int) * float
4. (int * int) * int * int
5. (int -> int) * int * int
6. (int -> int) * int -> int
Exercise 36
Integer division leaves a remainder. It is sometimes useful to calculate both the result of
the quotient and the remainder. Define a function div_mod : int -> int -> (int *
int) that takes two integers and returns a pair of their division and the remainder. For
instance,
# div_mod 40 20 ;;
- : int * int = (2, 0)
# div_mod 40 13 ;;
- : int * int = (3, 1)
# div_mod 0 12 ;;
- : int * int = (0, 0)
Using this technique of returning a pair, we can get the effect of a function that returns
multiple values.
Exercise 37
In Exercise 30, you are asked to implement the computus to calculate the month and
day of Easter for a given year by defining two functions, one for the day and one for the
year. A more natural approach is to define a single function that returns both the month
and the day. Use the technique from Exercise 36 to implement a single function for
computus.
S T RU C T U R E D D ATA A N D C O M P O S I T E T Y P E S 73
In the pattern x, y, the variables x and y are names that can be used
for the two components of the pair, as they have been in the expression
x + y. There is nothing special about the names x and y; any variables
could be used.
The match used here is especially simple in having just a single
pattern/result pair. Only one is needed because there is only one value
constructor for pairs. We’ll shortly see examples where more than one
pattern is used.
Notice how the match construction allows us to deconstruct a struc-
tured datum into its component parts simply by matching against
a template that uses the very same value constructor that is used to
74 PROGRAMMING WELL
build such data in the first place. This method for decomposition is
extremely general. It allows extracting the component parts from arbi-
trarily structured data.
You might think, for instance, that it would be useful to have a
function that directly extracts the first or second element of a pair. But
these can be written in terms of the match construct.3 3
The functions fst and snd are avail-
able as part of the Stdlib module, but
# let fst (pair : int * int) : int = it’s useful to see how they can be writ-
# match pair with ten in terms of the core of the OCaml
# | x, y -> x ;; language.
Line 3, characters 5-6:
3 | | x, y -> x ;;
^
Warning 27: unused variable y.
val fst : int * int -> int = <fun>
# fst (3, 4) ;;
- : int = 3
Exercise 38
Define an analogous function snd : int * int -> int that extracts the second
element of an integer pair. For instance,
# snd (3, 4) ;;
- : int = 4
let distance p1 p2 =
match p1 with
| x1, y1 ->
match p2 with
| x2, y2 -> ...calculate the distance... ;;
Rather than use two separate pattern matches, one for each argument,
we can perform both matches at once using a pattern that matches
against the pair of points p1, p2.
let distance p1 p2 =
match p1, p2 with
| (x1, y1), (x2, y2) -> ...calculate the distance... ;;
Once the separate components of the points are in hand, the distance
can be calculated:
# let distance p1 p2 =
# match p1, p2 with
# | (x1, y1), (x2, y2) ->
# sqrt ((x2 -. x1) ** 2. +. (y2 -. y1) ** 2.) ;;
val distance : float * float -> float * float -> float = <fun>
let distance p1 p2 =
let (x1, y1), (x2, y2) = p1, p2 in
sqrt ((x2 -. x1) ** 2. +. (y2 -. y1) ** 2.) ;;
Exercise 39
Simplify the definitions of addpair and fst above by taking advantage of this syntactic
sugar.
Using this shorthand can make code much more readable, and
is thus recommended. See the style guide (Section B.4.2) for further
discussion.
Exercise 40
Define a function slope : float * float -> float * float -> float that returns
the slope between two points.
Not only composite types can be the object of pattern matching. Pat-
terns can match particular values of atomic types as well, such as int
or bool. One could, for instance, write
# int_of_bool true ;;
- : int = 1
# int_of_bool false ;;
- : int = 0
# | 1 -> true
# | 2 -> true
# | _ -> false ;;
val is_small_int : int -> bool = <fun>
# is_small_int ~-1 ;;
- : bool = true
# is_small_int 2 ;;
- : bool = true
# is_small_int 7 ;;
- : bool = false
7.3 Lists
::
1 ::
2 ::
4 []
You can verify the equivalence of these notations by entering them into
OCaml:
# 1 :: (2 :: (4 :: [])) ;;
- : int list = [1; 2; 4]
# 1 :: 2 :: 4 :: [] ;;
- : int list = [1; 2; 4]
# [1; 2; 4] ;;
- : int list = [1; 2; 4]
Notice that in all three cases, OCaml provides the inferred type int
list and reports the value using the sugared list notation.8 8
The list containing elements, say, 1
and 2 – written [1; 2] – should not
Exercise 41 be confused with the pair of those
Which of the following expressions are well-formed, and for those that are, what are their same elements – written (1, 2). The
types and how would their values be written using the sugared notation? concrete syntactic differences may
be subtle (semicolon versus comma;
1. 3 :: []
brackets versus parentheses) but their
2. true :: false respective types make the distinction
3. true :: [false] quite clear.
4. [true] :: [false]
5. [1; 2; 3.1416]
6. [4; 2; -1; 1_000_000]
7. ([true], false)
Now we need to determine whether lst is empty or not, that is, what
value constructor was used to construct it. We can do so by pattern
matching lst against a series of patterns. Since lists have only two
value constructors, two patterns will be sufficient.
What should we do in these two cases? In the first case, we can con-
clude that lst is empty, and hence, the value of the function should 9
We’ve used alignment of the arrows
be true. In the second case, lst must have at least one element (now in the pattern match to emphasize the
named head by the pattern match), and is thus non-empty; the value parallelism between these two cases.
See the discussion in the style guide
of the function should be false.9 (Section B.1.7) for differing views on this
practice.
# let is_empty (lst : int list) : bool =
# match lst with
# | [] -> true
# | head :: tail -> false ;;
Line 4, characters 2-6:
4 | | head :: tail -> false ;;
^^^^
Warning 27: unused variable head.
Line 4, characters 10-14:
4 | | head :: tail -> false ;;
^^^^
Warning 27: unused variable tail.
val is_empty : int list -> bool = <fun>
10
The “wild card” anonymous variable
Since neither head nor tail are used in the second pattern match, _ is special in not serving as a name
they should be made anonymous variables to codify that intention that can be later referred to, and is thus
10 allowed to be used more than once in a
(and avoid a warning message). pattern.
# let is_empty (lst : int list) : bool =
# match lst with
# | [] -> true
# | _ :: _ -> false ;;
val is_empty : int list -> bool = <fun>
80 PROGRAMMING WELL
# is_empty [] ;;
- : bool = true
# is_empty [1; 2; 3] ;;
- : bool = false
# is_empty (4 :: []) ;;
- : bool = false
And again, a pattern match on the sole argument is a natural first step
to decide how to proceed in the calculation.
let length (lst : int list) : int =
match lst with
| [] -> ...
| hd :: tl -> ...
In the first match case, the list is empty; hence its length is 0.
let length (lst : int list) : int =
match lst with
| [] -> 0
| hd :: tl -> ...
# length [1; 2; 4] ;;
- : int = 3
# length [] ;;
- : int = 0
# length [[1; 2; 4]] ;;
Line 1, characters 8-17:
1 | length [[1; 2; 4]] ;;
^^^^^^^^^
Error: This expression has type 'a list
but an expression was expected of type int
Exercise 42
Why does this last example cause an error, given that its input is a list of length one?
Chapter 9 addresses this problem more thoroughly.
As a final example, we’ll implement a function that, given a list of
pairs of integers, returns the list of products of the pairs. For example,
the following behaviors should hold.
# prods [2,3; 4,5; 0,10] ;;
- : int list = [6; 20; 0]
# prods [] ;;
- : int list = []
By now the process should be familiar. Start with the type of the
function: (int * int) list -> int list. Use the type to write the
function introduction:
let rec prods (lst : (int * int) list) : int list = ...
In the first pattern match, the list is empty; we should thus return the
empty list of products.
let rec prods (lst : (int * int) list) : int list =
match lst with
| [] -> []
| hd :: tl -> ...
Finally, we get to the tricky bit. If the list is nonempty, the head will be
a pair of integers, which we’ll want access to. We could pattern match
against hd to extract the parts:
let rec prods (lst : (int * int) list) : int list =
match lst with
| [] -> []
| hd :: tl ->
match hd with
| (x, y) -> ...
82 PROGRAMMING WELL
but it’s simpler to fold that pattern match into the list pattern match
itself:
Now, the result in the second pattern match should be a list of integers,
the first of which is x * y and the remaining elements of which are the
products of the pairs in tl. The latter can be computed recursively as
prods tl. (It’s a good thing we thought ahead to use the rec keyword.)
Finally, the list whose first element is x * y and whose remaining
elements are prods tl can be constructed as x * y :: prods tl.
3. Write down the first line of the function definition, based on the
type of the function, which provides the argument and result types.
Exercise 43
Define a function sum : int list -> int that computes the sum of the integers in its
list argument.
S T RU C T U R E D D ATA A N D C O M P O S I T E T Y P E S 83
# sum [1; 2; 4; 8] ;;
- : int = 15
What should this function return when applied to the empty list?
Exercise 44
Define a function prod : int list -> int that computes the product of the integers
in its list argument.
# prod [1; 2; 4; 8] ;;
- : int = 64
What should this function return when applied to the empty list?
Exercise 45
Define a function sums : (int * int) list -> int list, analogous to prods
above, which computes the list each of whose elements is the sum of the elements of the
corresponding pair of integers in the argument list. For example,
# sums [2,3; 4,5; 0,10] ;;
- : int list = [5; 9; 10]
# sums [] ;;
- : int list = []
Exercise 46
Define a function inc_all : int list -> int list, which increments all of the
elements in a list.
# inc_all [1; 2; 4; 8] ;;
- : int list = [2; 3; 5; 9]
Exercise 47
Define a function square_all : int list -> int list, which squares all of the
elements in a list.
# square_all [1; 2; 4; 8] ;;
- : int list = [1; 4; 16; 64]
Exercise 48
Define a function append : int list -> int list -> int list to append two
integer lists. Some examples:
# append [1; 2; 3] [4; 5; 6] ;;
- : int list = [1; 2; 3; 4; 5; 6]
# append [] [4; 5; 6] ;;
- : int list = [4; 5; 6]
# append [1; 2; 3] [] ;;
- : int list = [1; 2; 3]
Exercise 49
Define a function concat : string -> string list -> string, which takes a
string sep and a string list lst, and returns one string with all the elements of lst
concatenated together but separated by the string sep.12 Some examples: 12
The OCaml library module String
# concat ", " ["first"; "second"; "third"] ;; already provides just this function under
- : string = "first, second, third" the same name.
# concat "..." ["Moo"; "Baa"; "Lalala"] ;;
- : string = "Moo...Baa...Lalala"
# concat ", " [] ;;
- : string = ""
# concat ", " ["Moo"] ;;
- : string = "Moo"
7.4 Records
Tuples and lists use the order within a sequence to individuate their
elements. An alternative, R E C O R D S , name the elements, providing
each with a unique label. The type constructor specifies the labels and
the type of element associated with each. For instance, suppose we’d
like to store information about people: first and last name and year of
birth. An appropriate record type would be
# let ac =
# {firstname = "Alonzo";
# lastname = "Church";
# birthyear = 1903} ;;
val ac : person =
{lastname = "Church"; firstname = "Alonzo"; birthyear = 1903}
S T RU C T U R E D D ATA A N D C O M P O S I T E T Y P E S 85
Notice that the type inferred for ac is person, the defined name for the
record type.
As usual, we use pattern matching to decompose a record into its
constituent parts. A simple example decomposes the ac value just
created to extract the birth year.
# match ac with
# | {lastname = lname;
# firstname = fname;
# birthyear = byear} -> byear ;;
Line 2, characters 15-20:
2 | | {lastname = lname;
^^^^^
Warning 27: unused variable lname.
Line 3, characters 12-17:
3 | firstname = fname;
^^^^^
Warning 27: unused variable fname.
- : int = 1903
The warnings remind us that there are unused variables that should be
explicitly marked as such:
# match ac with
# | {lastname = _lname;
# firstname = _fname;
# birthyear = byear} -> byear ;;
- : int = 1903
# fullname ac ;;
- : string = "Alonzo Church"
# ac.firstname ;;
- : string = "Alonzo"
# ac.birthyear ;;
- : int = 1903
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
7.6.1 Background
I’m an apple farmer who hates apples but loves broccoli. You’re a
broccoli farmer who hates broccoli but loves apples. The obvious
solution to this sad state of affairs is for us to trade – I ship you a box of
my apples and you ship me a box of your broccoli. Win-win.
But I might try to get clever by shipping an empty box. Instead of
cooperating, I “defect”. I still get my broccoli from you (assuming you
don’t defect) and get to keep my apples. You, thinking through this
scenario, realize that you’re better off defecting as well; at least you’ll
get to keep your broccoli. But then, nobody gets what we want; we’re
both worse off. The best thing to do in this D O N AT I O N G A M E seems to
be to defect.
It’s a bit of a mystery, then, why people cooperate at all. The answer
may lie in the fact that we engage in many rounds of the game. If you
get a reputation for cooperating, others may be willing to cooperate as
well, leading to overall better outcomes for all involved.
88 PROGRAMMING WELL
prisoner’s dilemma that allows for testing different payoff matrices and
strategies.
Thorough testing is important in all your work. Testing will help you
find bugs, avoid mistakes, and teach you the value of short, clear func-
tions. In the file ps1_tests.ml, we’ve put some prewritten tests for
one of the practice functions using the testing method of Section 6.5.
Spend some time understanding how the testing function works and
why these tests are comprehensive. Then, for each function in Prob-
lem 1, write tests that thoroughly test the functionality of each of the
remaining sections of Problem 1, covering all code paths and corner
cases.
To run your tests, run the shell command
% make ps1_tests
in the directory where your ps1.ml is located and then run the com-
piled program by executing ps1_tests.byte:
% ./ps1_tests.byte
The program should then generate a full report with all of the unit tests
for all of the functions in the problem set.
7.6.5 Tournament
To allow you to see how your custom strategy fares, we will run a
course-wide round-robin tournament in which your strategy will be
run against every other student’s strategy for a random number of
rounds. To keep things interesting, we will add a small amount of noise
to each strategy: 5% of the time, we will use the opposite action of the
one a strategy specifies.
The tournament is pseudonymized and optional. If you want to
participate, you’ll provide a pseudonym for your entrant. If you specify
an empty pseudonym, we won’t enter you in the tournament, though
we encourage you to come up with clever strategies to compete against
your fellow classmates! We’ll post and regularly update a leaderboard,
displaying each student’s pseudonym and their strategy’s average
payoff. The top score will win a rare and wonderful prize!
7.6.6 Testing
You should again provide unit tests in ps1_tests.ml for each function
that you implement in Problem 2.
In addition, you can choose to uncomment the main function in
ps1.ml and then recompile the file by running make ps1.byte in
your shell. Then, run the command ./ps1.byte and you should see
via printed output that Nasty earns a payoff of 500 and Patsy earns
a payoff of −200. You can change the strategies played to see how
different strategies perform against each other.
8
Higher-order functions and functional programming
Edict of irredundancy:
Never write the same code twice.
Having written the same code twice, all of the problems of debugging,
maintaining, documenting, and testing code have been similarly
multiplied.
The edict of irredundancy is the principle of avoiding the problems
introduced by duplicative code. Rather than write the same code twice,
the edict calls for viewing the apparently dissimilar pieces of code as
instantiating an underlying identity, and factoring out the common
parts using an appropriate abstraction mechanism.
Given the emphasis in the previous chapters, it will be unsurprising
to see that the abstraction mechanism we turn to first is the function
itself. By examining some cases of similar code, we will present the
use of higher-order functions to achieve the abstraction, in so doing
presenting some of the most well known abstractions of higher-order
functional programming on lists – map, fold, and filter.
you may have thought to cut and paste the solution, modifying it
slightly to solve the second:
# let rec square_all (xs : int list) : int list =
# match xs with
# | [] -> []
# | hd :: tl -> (hd * hd) :: (square_all tl) ;;
val square_all : int list -> int list = <fun>
# let rec map (f : int -> int) (xs : int list) : int list =
# match xs with
# | [] -> []
# | hd :: tl -> f hd :: (map f tl) ;;
val map : (int -> int) -> int list -> int list = <fun>
The map function takes two arguments (curried, that is, one at a time),
the first of which is itself a function, to be applied to all elements of its
second integer list argument. Its type is thus (int -> int) -> int
list -> int list. With map in hand, we can perform the equivalent
of inc_all and square_all directly.
# map (fun x -> 1 + x) [1; 2; 4; 8] ;;
- : int list = [2; 3; 5; 9]
# map (fun x -> x * x) [1; 2; 4; 8] ;;
- : int list = [1; 4; 16; 64]
In fact, map can even be used to define the functions inc_all and
square_all.
demonstrating that all along, power was a function (defined with fun)
of one argument (now called arg).
How about this definition of power?
Again, desugaring reveals that all of the functions in the definition take
a single argument.
But since power is curried, we can define the cube function even more
simply, by applying the power function to its “first” argument only.
# let cube = power 3 ;;
val cube : int -> int = <fun>
# cube 4 ;;
- : int = 64
Exercise 50
A T E S S E R A C T is the four-dimensional analog of a cube, so fourth powers of numbers are
sometimes referred to as T E S S E R A C T I C N U M B E R S . Use the power function to define a
function tesseract that takes its integer argument to the fourth power.
Now, map is itself a curried function and therefore can itself be par-
tially applied to its first argument. It takes its function argument and
its list argument one at a time, and applying it only to its first argu-
ment generates a function that applies that argument function to all
96 PROGRAMMING WELL
# inc_all [1; 2; 4; 8] ;;
- : int list = [2; 3; 5; 9]
# square_all [1; 2; 4; 8] ;;
- : int list = [1; 4; 16; 64]
Exercise 51
Use the map function to define a function double_all that takes an int list argument
and returns a list with the elements doubled.
Let’s take a look at some other functions that bear a striking resem-
blance. Exercises 43 and 44 asked for definitions of functions that took,
respectively, the sum and the product of the elements in a list. Here are
some possible solutions, written in the recursive style of Chapter 7:
# | [] -> 0
# | hd :: tl -> hd + (sum tl) ;;
val sum : int list -> int = <fun>
The prod function, similarly, is a kind of fold, this time of the prod-
uct function starting with the multiplicative identity 1.
98 PROGRAMMING WELL
This function matches the fold structure as well. The initial value, the
length of an empty list, is 0, and the operation to apply to the head of
the list and the recursively processed tail is to simply ignore the head
and increment the value for the tail.
# let length lst = fold (fun _hd tlval -> 1 + tlval) lst 0 ;;
val length : int list -> int = <fun>
# length [1; 2; 4; 8] ;;
- : int = 4
Exercise 52
Define the higher-order function fold_left : (int -> int -> int) -> int -> int
list -> int, which performs this left-to-right fold.
but (because the list argument of fold_left is the final argument) this
can be further simplified by partial application:
Exercise 53
Define the length function that returns the length of a list, using fold_left.
Exercise 54
A cousin of the fold_left function is the function reduce,3 which is like fold_left 3
The higher-order functional program-
except that it uses the first element of the list as the initial value, calculating ming paradigm founded on functions
like map and reduce inspired the wildly
(f ⋯ (f (f x_1 x_2) x_3) x_n) . popular Google framework for parallel
Define the higher-order function reduce : (int -> int -> int) -> int list -> processing of large data sets called, not
int, which works in this way. You might define reduce recursively as we did with fold surprisingly, MapReduce (Dean and
and fold_left or nonrecursively by using fold_left itself. (By its definition reduce is Ghemawat, 2004).
undefined when applied to an empty list, but you needn’t deal with this case where it’s
applied to an invalid argument.)
Exercise 55
100 PROGRAMMING WELL
Define a function filter : (int -> bool) -> int list -> int list that returns
a list containing all of the elements of its second argument for which its first argument
returns true.
Exercise 56
Provide definitions of evens, odds, positives, and negatives in terms of filter.
Exercise 57
Define a function reverse : int list -> int list, which returns the reversal of its
argument list. Instead of using explicit recursion, define reverse by mapping, folding, or
filtering.
Exercise 58
Define a function append : int list -> int list -> int list (as described in
Exercise 48) to calculate the concatenation of two integer lists. Again, avoid explicit
recursion, using map, fold, or filter functions instead.
We’ve used the same technique three times in this chapter – notic-
ing redundancies in code and carving out the differing bits to find
the underlying commonality. Determining the best place to carve is
an important skill, the basis for R E F A C T O R I N G of code, which is the
name given to exactly this practice. And it turns out to match Socrates’s
second principle in Phaedrus:
P H A E D RU S :
And what is the other principle, Socrates?
S O C R AT E S :
That of dividing things again by classes, where the natural
joints are, and not trying to break any part after the manner of a bad
carver. (Plato, 1927)
Edict of decomposition:
Carve software at its joints.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
H I G H E R- O R D E R F U N C T I O N S A N D F U N C T I O N A L P RO G R A M M I N G 101
What happens when the edict of intention runs up against the edict
of irredundancy? The edict of intention calls for expressing clearly the
intended types over which functions operate, so that the language can
provide help by checking that the types are used consistently. We’ve
heeded that edict, for example, in our definition of the higher-order
function map from the previous chapter, repeated here:
# let rec map (f : int -> int) (xs : int list) : int list =
# match xs with
# | [] -> []
# | hd :: tl -> f hd :: (map f tl) ;;
val map : (int -> int) -> int list -> int list = <fun>
but we run afoul of the typing constraints on map, which can only apply
functions of type int -> int, and not float -> float or int * int
-> int.
104 PROGRAMMING WELL
# let succ x = x + 1 ;;
val succ : int -> int = <fun>
it follows from the fact that the + function is applied to x that x must
have the same type as the argument type for +, that is, int. Similarly,
since succ x is calculated as the output of the + function, it must have
the same type as +’s output type, again int. Since succ’s argument is of
type int and output is of type int, its type must be int -> int. And
in fact that is the type OCaml reports for it, even though no explicit
typings were provided.
P O LY M O R P H I S M A N D G E N E R I C P R O G R A M M I N G 105
# let id x = x ;;
val id : 'a -> 'a = <fun>
# let rec map (f : 'a -> 'b) (xs : 'a list) : 'b list =
# match xs with
# | [] -> []
# | hd :: tl -> f hd :: (map f tl) ;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
The type variables make clear the intended constraints among f, xs,
and the return value f xs.
Problem 59
For each of the following types construct an expression for which OCaml would infer
that type. (No cheating by using explicit typing annotations with the : operator!) The ex-
pressions need not be practical or do anything useful; they need only have the requested
type. For example, for the type bool * bool, the expression true, true would be a
possible answer.
Exercise 60
Define polymorphic versions of fold and filter, providing explicit polymorphic typing
information.
Exercise 61
Perhaps surprisingly, the map function can itself be written in terms of fold. Provide a
definition for map that involves just a single call to fold.
Problem 62
For each of the following definitions of a function f, give its most general type (as would
be inferred by OCaml) or explain briefly why no type exists for the function.
1. let f x =
x +. 42. ;;
2. let f g x =
g (x + 1) ;;
3. let f x =
match x with
| [] -> x
| h :: t -> h ;;
4. let rec f x a =
match x with
| [] -> a
| h :: t -> h (f t a) ;;
5. let f x y =
match x with
| (w, z) -> if w then y z else w ;;
6. let f x y =
x y y ;;
7. let f x y =
x (y y) ;;
8. let rec f x =
match x with
| None
| Some 0 -> None
| Some y -> f (Some (y - 1)) ;;
9. let f x y =
if x then [x]
else [not x; y] ;;
One way, perhaps the best, for satisfying the edict of irredundancy
is to avoid writing the same code twice by not writing the code even
once, instead taking advantage of code that someone else has already
written. OCaml, like many modern languages, comes with a large set
of libraries (packaged as modules, which we’ll cover in Chapter 12)
that provide a wide range of functions. The List module in particular
provides exactly the higher-order list processing functions presented
108 PROGRAMMING WELL
The List library has further functions for sorting, combining, and
transforming lists in all kinds of ways.
Although these functions are built into OCaml through the List
library, it’s still useful to have seen how they are implemented and
why they have the types they have. In particular, it makes clear that
the power of list processing via higher-order functional programming
doesn’t require special language constructs; they arise from the in-
teractions of simple language primitives like first-class functions and
structured data types.
P O LY M O R P H I S M A N D G E N E R I C P R O G R A M M I N G 109
Problem 63
Provide an implementation of the List.map function over a list using only a call to
List.fold_right over the same list, or provide an argument for why it’s not possible to
do so.
Problem 64
Provide an implementation of the List.fold_right function using only a call to
List.map over the same list, or provide an argument for why it’s not possible to do so.
Problem 65
In the list module, OCaml provides a function partition : (’a -> bool) -> ’a
list -> ’a list * ’a list. According to the OCaml documentation, “partition p
l returns a pair of lists (l1, l2), where l1 is the list of all the elements of 1 that satisfy
the predicate p, and 12 is the list of all the elements of l that do not satisfy p. The order of
the elements in the input list is preserved.”
For example, we can use this to divide a list into two new ones, one containing the
even numbers and one containing the odd numbers:
# List.partition (fun n -> n mod 2 = 0)
# [1; 2; 3; 4; 5; 6; 7] ;;
- : int list * int list = ([2; 4; 6], [1; 3; 5; 7])
As described above, the List module provides the partition function of type (’a ->
bool) -> ’a list -> ’a list * ’a list. Give your own definition of partition,
implemented directly without the use of any library functions except for those in the
Stdlib module.
Exercise 66
Define a function permutations : ’a list -> ’a list list, which takes a list of
values and returns a list containing every permutation of the original list. For example,
# permutations [1; 2; 3] ;;
- : int list list =
[[1; 2; 3]; [2; 1; 3]; [2; 3; 1]; [1; 3; 2]; [3; 1; 2]; [3; 2; 1]]
It doesn’t matter what order the permutations appear in the returned list. Note that if
the input list is of length n, then the answer should be of length n ! (that is, the factorial
of n). Hint: One way to do this is to write an auxiliary function, interleave : int ->
int list -> int list list, that yields all interleavings of its first argument into its
second. For example:
# interleave 1 [2; 3] ;;
- : int list list = [[1; 2; 3]; [2; 1; 3]; [2; 3; 1]]
Problem 67
Provide an OCaml definition for a higher-order function @+ that takes two functions
as arguments and returns their composition. The function should have the following
behavior:
# let weighted_sum = sum @+ prods ;;
val weighted_sum : (int * int) list -> int = <fun>
# weighted_sum [(1, 3); (2, 4); (3, 5)] ;;
- : int = 26
Notice that by naming the function @+, it is used as an infix, right-associative operator.
See the operator table in the OCaml documentation for further information about the
syntactic properties of operators. When defining the function itself, though, you’ll want
to use it as a prefix operator by wrapping it in parentheses, as (@+).
Problem 68
What is the type of the @+ function?
Here’s an interesting bit of trivia: Not all credit card numbers are well-
formed. The final digit in a 16-digit credit card number is in fact a
C H E C K S U M , a digit that is computed from the previous 15 by a simple
algorithm.
The algorithm used to generate the checksum is called the L U H N
C H E C K. To calculate the correct final checksum digit used in a 16-digit
credit card number, you perform the following computation on the
first 15 digits of the credit card number:
2. Add all of the even position digits and the doubled odd position
digits together. For the example above, the sum would be
(2 + 5 + 1 + 6 + 3 + 2 + 4) + (8 + 5 + 6 + 1 + 0 + 5 + 1 + 9) = 23 + 35 = 58 .
3. The checksum is then the digit that when added to this sum makes
it a multiple of ten. In the example above the checksum would be
2, since adding 2 to 58 generates 60, which is a multiple of 10. Thus,
P O LY M O R P H I S M A N D G E N E R I C P R O G R A M M I N G 111
the sequence 4275 3156 0372 5492 is a valid credit card number, but
changing the last digit to any other makes it invalid. (In particular,
the final 3 in the card in Figure 9.2 is not the correct checksum!)
Problem 69
Define an explicitly recursive polymorphic function odds to extract the elements at
odd-numbered indices in a list, where the indices are counted starting with 1, so that
# let cc = [4; 2; 7; 5; 3; 1; 5; 6; 0; 3; 7; 2; 5; 4; 9] ;;
val cc : int list = [4; 2; 7; 5; 3; 1; 5; 6; 0; 3; 7; 2; 5; 4; 9]
# odds cc ;;
- : int list = [4; 7; 3; 5; 0; 7; 5; 9]
Exercise 70
What is the type of the odds function?
In addition to odds, it will be useful to have a function evens that
extracts the elements at even-numbered indices in a list.
Problem 71
Define evens to extract the elements at even-numbered indices in a list, where the
indices are counted starting with 1, so that
# let cc = [4; 2; 7; 5; 3; 1; 5; 6; 0; 3; 7; 2; 5; 4; 9] ;;
val cc : int list = [4; 2; 7; 5; 3; 1; 5; 6; 0; 3; 7; 2; 5; 4; 9]
# evens cc ;;
- : int list = [2; 5; 1; 6; 3; 2; 4]
All the parts are now in place to implement the Luhn check algo-
rithm.
Problem 73
Implement a function luhn that takes a list of integers and returns the check digit
for that digit sequence. (You can assume that it is called with a list of 15 integers.) For
instance, for the example above
# luhn cc ;;
- : int = 2
You should feel free to use the functions evens, odds, doublemod9, sum, and any other
OCaml library functions that you find useful and idiomatic.
Problem 74
Now that you know how to generate valid credit card numbers not your own, do you
think it would be legal for you to use these numbers on an e-commerce web site to test
whether your implementation is correct? Would it be ethical for you to do so?
Exercise 75
What are the types of the hd and tl functions? See if you can determine them without
looking them up.
These can be composed to allow, for instance, extracting the head of
the tail of a list, that is, the list’s second item.
# let second = List.hd @+ List.tl ;;
val second : '_weak1 list -> '_weak1 = <fun>
but why did the typing of second have those oddly named type vari-
ables?
Type variables like ’_weak1 (with the initial underscore) are W E A K
T Y P E VA R I A B L E S , not true type variables. They maintain their poly-
morphism only temporarily, until the first time they are applied. Weak
type variables arise because in certain situations OCaml’s type infer-
ence can’t figure out how to express the most general types and must
resort to this fallback approach.
When a function with these weak type variables is applied to argu-
ments with a specific type, the polymorphism of the function disap-
pears. Having applied second to an int list, OCaml further instanti-
ates the type of second to only apply to int list arguments, losing its
polymorphism. We can see this in two ways, first by checking its type
directly,
# second ;;
- : int list -> int = <fun>
For the curious, if you want to see what’s going on in detail, you can
check out the discussion in the section “A function obtained through
partial application is not polymorphic enough” in the OCaml FAQ.
10
Handling anomalous conditions
The function works fine most of the time, but there is one anoma-
lous condition to consider, where the median isn’t well defined: What
should the median function do on the empty list?
116 PROGRAMMING WELL
There are two problems. First, the method can lead to gratuitous type
instantiation; second, and more critically, it manifests in-band signal-
ing.
Check the types inferred for the two versions of median above. The
original is appropriately polymorphic, of type ’a list -> ’a. But
because the error value cERROR used in the second version is of type
int, median becomes instantiated to int list -> int. The code
no longer applies outside the type of the error value, restricting its
generality and utility. And there is a deeper problem.
Consider the sad fate of poor Christopher Null, a technology jour-
nalist with a rather inopportune name. Apparently, there is a fair
amount of software that uses the string "null" as an error value for
cases in which no last name was provided. Errors can then be checked
for using code like
if last_name = "null" then ...
Maybe you see the problem. Poor Mr. Null reports that
that users of the median function can’t tell the difference between the
value being the true median or the median being undefined.
Having dismissed the in-band error signaling approach, we turn to
better solutions.
The first approach, like the in-band error value approach, still handles
the problem explicitly, right in the return value of the function. How-
ever, rather than returning an in-band value, an int (or whatever the
type of the list elements is), the function will return an out-of-band
None value, that has been added to the int type to form an optional
int, a value of type int option.
Option types are another kind of structured type, beyond the lists,
tuples, and records from Chapter 7. The postfix type constructor
option creates an option type from a base type, just as the postfix
type constructor list does. There are two value constructors for op-
tion type values: None (connoting an anomalous value), and the prefix
value constructor Some. The argument to Some is a value of the base
type.
For the median function, we’ll use an int option as the return
value, or, more generically, an ’a option. In the anomalous condition,
we return None, and in the normal condition in which a well-defined
median v can be computed, we return Some v.
Exercise 76
Why do you think nth was designed so as to take its list argument before its index
argument?
If we were to reimplement this function, it might look something
like this:
# let rec nth (lst : 'a list) (n : int) : 'a =
# match lst with
# | hd :: tl ->
# if n = 0 then hd
# else nth tl (n - 1) ;;
Lines 2-5, characters 0-19:
2 | match lst with
3 | | hd :: tl ->
4 | if n = 0 then hd
5 | else nth tl (n - 1)...
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val nth : 'a list -> int -> 'a = <fun>
# nth_opt [1; 2; 3] 1 ;;
- : int option = Some 2
# nth_opt [1; 2; 3] 5 ;;
- : int option = None
Exercise 77
Another anomalous condition for nth and nth_opt is the use of a negative index. What
currently is the behavior of nth_opt with negative indices? Revise the definition of
nth_opt to appropriately handle this case as well.
Exercise 78
Define a function last_opt : ’a list -> ’a option that returns the last element in
a list (as an element of the option type) if there is one, and None otherwise.
# last_opt [] ;;
- : 'a option = None
# last_opt [1; 2; 3; 4; 5] ;;
- : int option = Some 5
Exercise 79
The variance of a sequence of n numbers x 1 , . . . , x n is given by the following equation:
∑ni=1 (x i − m )2
n −1
where n is the number of elements in the sequence, m is the arithmetic mean (or
average) of the elements in the sequence, and x i is the i -th element in the sequence.
The variance is only well defined for sequences with two or more elements. (Do you see
why?)
Define a function variance : float list -> float option that returns None
if the list has fewer than two elements. Otherwise, it should return the variance of the
numbers in its list argument, wrapped appropriately for its return type.4 For example: 4
If you want to compare your output
# variance [1.0; 2.0; 3.0; 4.0; 5.0] ;; with an online calculator, make sure you
- : float option = Some 2.5 find one that calculates the (unbiased)
# variance [1.0] ;; sample variance.
- : float option = None
Remember to use the floating point version of the arithmetic operators when operating
on floats (+., *., etc). The function float can convert (“cast”) an int to a float.
^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type int option
but an expression was expected of type int
What about the function that called the one that raised the excep-
tion? It is expecting a value of a certain type to be returned, but in this
case, no such value is supplied. The calling function thus can’t return
either. It stops too. And so on and so forth.
We can write a version of nth that raises an exception when the
index is too large.
# let rec nth (lst : 'a list) (n : int) : 'a =
# match lst with
# | [] -> raise Exit
# | hd :: tl ->
# if n = 0 then hd
# else nth tl (n - 1) ;;
val nth : 'a list -> int -> 'a = <fun>
# nth [1; 2; 3] 1 ;;
- : int = 2
# nth [1; 2; 3] 5 ;;
Exception: Stdlib.Exit.
# (nth [0; 1; 2] (nth [1; 2; 3] 1)) + 1 ;;
- : int = 3
There are several things to notice here. First, the return type of nth re-
mains ’a, not ’a option. Under normal conditions, it returns the n-th
element itself, not an option-wrapped version thereof. This allows its
use in embedded applications (as in the third example above) without
leading to the dreaded option poisoning. When an error does occur, as
in the second example, execution stops and a message is printed by the
OCaml R E P L (“Exception: Stdlib.Exit.”) describing the exception
that was raised, namely, the Exit exception defined in the Stdlib li-
brary module. No value is returned from the computation at all, so no
value is ever printed by the R E P L .
The code that actually raises the Exit exception is in the third line
of nth: raise Exit. The built-in raise function takes as argument an
expression of type exn, the type for exceptions. As it turns out, Exit is
a value of that type, as can be verified directly:
# Exit ;;
- : exn = Stdlib.Exit
appropriate to use when the index of nth is too large for the given
list.
Returning to the median example above, and repeated here for refer-
ence (but this time using our own implementation of nth),
this code doesn’t use option types and doesn’t use the raise func-
tion to raise any exceptions. What does happen when the anomalous
condition occurs?
# median [] ;;
Exception: Failure "nth: index too large".
HANDLING ANOMALOUS CONDITIONS 123
An exception was raised, not by the median function, but by our nth
function that it calls, which raises a Failure exception when it is called
to take an element of the empty list. The exception propagates from
the nth call to the median call to the top level of the R E P L .
Perhaps you, as the writer of some code, have an idea about how
to handle particular anomalies that might otherwise raise an excep-
tion. Rather than allow the exception to propagate to the top level,
you might want to handle the exception yourself. The try...with
construct allows for this.
The syntax of the construction is
try ⟨expr ⟩
with ⟨match ⟩
# nth_opt [1; 2; 3] 0 ;;
- : int option = Some 1
# nth_opt [1; 2; 3] (-1) ;;
- : int option = None
# nth_opt [1; 2; 3] 4 ;;
- : int option = None
Let’s try to define the function, starting with its type. The zip func-
tion takes two lists, with types, say, ’a list and ’b list, and returns
a list of pairs each of which has an element from the first list (of type
’a) and an element from the second (of type ’b). The pairs are thus of
type ’a * ’b and the return value of type (’a * ’b) list. The type
of the whole function, then, is ’a list -> ’b list -> (’a * ’b)
list. From this, the header follows directly.
If the lists are empty, the list of pairs of their elements is empty too.
Otherwise, the zip of the non-empty lists starts with the two heads
paired. The remaining elements are the zip of the tails.
You’ll notice that there’s an issue. And if you don’t notice, the inter-
preter will, as soon as we enter this definition:
There are missing match cases, in particular, when one of the lists is
empty and the other isn’t. This can arise whenever the two lists are of
different lengths. In such a case, the zip of two lists is not well defined.
As usual, we have two approaches to addressing the anomaly, with
options and with exceptions. We’ll pursue them in order.
We can make explicit the possibility of error values by returning an
option type.
The normal match cases can return their corresponding option type
value using the Some constructor.
Finally, we can add a wild-card match pattern for the remaining cases.
Exercise 80
Try to see if you can diagnose the problem before reading on.
The indentation of this code notwithstanding, the final pattern
match is associated with the inner match, not the outer one. The inner
match is, indeed, for list options. The intention was that only the lines
beginning | None... and | Some ... be part of that match, but the
next line has been caught up in it as well.
One simple solution is to use parentheses to make explicit the
intended structure of the code.
Better yet is to make explicit the patterns that fall under the wildcard
allowing them to move up in the ordering.
Exercise 81
Why is it necessary to make the patterns explicit before moving them up in the ordering?
What goes wrong if we leave the pattern as _, _?
As an alternative, we can implement zip to raise an exception on
lists of unequal length. Doing so simplifies the matches, since there’s
no issue of option poisoning.
Exercise 82
Define a function zip_safe that returns the zip of two equal-length lists, returning the
empty list if the arguments are of unequal length. The implementation should call zip.
Exceptions are first-class values, of the type exn. Like lists and options,
exceptions have multiple value constructors. We’ve seen some already:
Exit, Failure, Invalid_argument. (It’s for that reason that we can
pattern match against them in the try...with construct.)
Exceptions are exceptional in that new value constructors can be
added dynamically. Here we define a new exception value constructor:
# exception Timeout ;;
exception Timeout
Exercise 83
In Section 6.4, we noted a problem with the definition of fact for computing the
factorial function; it fails on negative inputs. Modify the definition of fact to raise an
exception to make that limitation explicit.
Exercise 84
What are the types of the following expressions (or the values they define)?
1. Some 42
2. [Some 42; None]
3. [None]
4. Exit
5. Failure "nth"
6. raise (Failure "nth")
7. raise
8. fun _ -> raise Exit
9. let failwith s =
raise (Failure s)
Problem 85
As in Problem 59, for each of the following OCaml function types define a function f
(with no explicit typing annotations, that is, no uses of the : operator) for which OCaml
would infer that type. (The functions need not be practical or do anything useful; they
need only have the requested type.)
1. int -> int -> int option
2. (int -> int) -> int option
3. ’a -> (’a -> ’b) -> ’b
4. ’a option list -> ’b option list -> (’a * ’b) list
HANDLING ANOMALOUS CONDITIONS 129
Problem 86
As in Problem 62, for each of the following function definitions of a function f, give
a typing for the function that provides its most general type (as would be inferred by
OCaml) or explain briefly why no type exists for the function.
1. let rec f x =
match x with
| [] -> f
| h :: t -> raise Exit ;;
2. let f x =
if x then (x, true)
else (true, not x) ;;
Problem 87
Provide a more succinct definition of the function f from Problem 86(2), with the same
type and behavior.
Which should you use when writing code to handle anomalous con-
ditions? Options or exceptions? This is a design decision. There is no
universal right answer.
Options are explicit: The type gives an indication that an anomaly
might occur, and the compiler can make sure that such anomalies are
handled. Exceptions are implicit: You (and the compiler) can’t tell if an
exception might be raised while executing a function. But exceptions
are therefore more concise. The error handling doesn’t impinge on the
data and so doesn’t poison every downstream use of the data. Code to
handle the anomaly doesn’t have to exist everywhere between where
the problem occurs and where it’s dealt with.
Which is more important, explicitness or concision? It depends.
# let nth_test () =
# unit_test (nth [5] 0 = 5) "nth singleton";
# unit_test (nth [1; 2; 3] 0 = 1) "nth start";
# unit_test (nth [1; 2; 3] 1 = 2) "nth middle" ;;
val nth_test : unit -> unit = <fun>
# nth_test () ;;
nth singleton passed
nth start passed
nth middle passed
- : unit = ()
# let nth_test () =
# unit_test (nth [5] 0 = 5) "nth singleton";
# unit_test (nth [1; 2; 3] 0 = 1) "nth start";
# unit_test (nth [1; 2; 3] 1 = 2) "nth middle";
# unit_test (nth [1; 2; 3] 2 = 3) "nth last" ;;
val nth_test : unit -> unit = <fun>
but this fails to type-check, since the type of the nth expression is int
(since it was applied to an int list), whereas the with clauses return
a bool. We’ll need to return a bool in the try as well. In fact, we should
return false; if nth [1; 2; 3] 4 manages to return a value and not
raise an exception, that’s a sign that nth has a bug! We revise the test
condition to be
# try let _ = nth [1; 2; 3] 4 in
# false
# with
# | Failure _ -> true
# | _ -> false ;;
- : bool = true
# nth_test () ;;
nth singleton passed
nth start passed
nth middle passed
nth last passed
nth index too big passed
- : unit = ()
132 PROGRAMMING WELL
We’ll later see more elegant ways to put together unit tests (Sec-
tion 17.6).
Exercise 88
Augment nth_test to verify that nth works properly under additional conditions: on the
empty list, with negative indexes, with lists other than integer lists, and so forth.
tuples ⟨⟩ * ⟨⟩ ⟨⟩ , ⟨⟩
⟨⟩ * ⟨⟩ * ⟨⟩ ⟨⟩ , ⟨⟩ , ⟨⟩
⋯
lists ⟨⟩ list []
⟨⟩ :: ⟨⟩
[ ⟨⟩ ; ⟨⟩ ; ...]
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
# min_int, max_int ;;
- : int * int = (-4611686018427387904, 4611686018427387903)
The int type can then represent integers with up to 18 or so digits, that
134 PROGRAMMING WELL
is, integers in the quintillions, but RSA needs integers with hundreds of
digits.
Computer representations for arbitrary size integers are tradition-
ally referred to as B I G N U M S . In this assignment, you will be imple-
menting bignums, along with several operations on bignums, includ-
ing addition and multiplication. We provide code that will use your
bignum implementation to implement the RSA cryptosystem. Once
you complete your bignum implementation, you’ll be able to encrypt
and decrypt messages using this public-key cryptosystem, and dis-
cover a hidden message that we’ve provided encoded in this way.
7
which we will represent by the list [123; 456; 789]. Notice that the We name the base using this distinc-
tive naming convention, with initial
least significant coefficient (789) appears last in the list. As another ‘c’ (for constant) and upper-case
example, the number 12000000000000 is represented by the list [12; mnemonic to emphasize that it is a
global constant. Such global constants
0; 0; 0; 0].
should be rare, and thus typographically
The base used by your bignum implementation is defined at the top distinctive.
of the file bignum.ml:7
The neg field specifies whether the number is positive (if neg is false)
or negative (if neg is true). The coeffs field is the list of coefficients
HANDLING ANOMALOUS CONDITIONS 135
• Coefficients are never negative and are always strictly less than
cBASE.
Functions that consume bignums may assume that they satisfy the
invariant. We will not test your code using bignums that violate the
invariant. Functions that return bignums should preserve these in-
variants. For example, your functions should never return a bignum
representing zero with the neg flag set to true or a bignum with a
coefficients list with leading zeros.
136 PROGRAMMING WELL
goal is correctness, so keep your code as simple as possible. Make sure your code works
with positive numbers, negative numbers, and zero. Assume that the arguments satisfy
5 4 3
the invariant. Hint: You may want to write a helper function that multiplies a bignum by
× 2 2 4
a single int (which might be one coefficient of a bignum).
2 1 7 2
+ 1 0 8 6 0
Using bignums to implement RSA We’ve provided an implementation + 1 0 8 6 0 0
of the RSA cryptosystem in the file rsa.ml. It uses the module Bignum, = 1 2 1 6 3 2
that is, the bignum implementation in bignum.ml that you’ve just
Figure 10.2: Multiplication of 543 and
completed. 224 using the grade school algorithm.
In the file rsa_puzzle.ml, we’ve placed some keys and a ciphertext First, you multiply the first number
(543 in this example) by each of the
with a secret message. If your implementation of bignums is correct, digits of the second number, from least
you should be able to compile and run the file: significant to most significant (4, then
2, then 2), adding an increasing number
% ocamlbuild rsa_puzzle.byte of zeros at the end (shown in italics),
no zeros for the least significant digit
% ./rsa_puzzle.byte (resulting in 2172), one for the next
(1086 plus one zero yielding 10860),
two for the next, and so forth. Then
to reveal the secret message! the partial products 2172, 10860, and
108600 are summed to generate the
final result 121632.
10.6.2 Challenge problem: Multiply faster
This section is intended for those who are interested in more math-
ematical details on how the RSA system uses bignum calculations to
achieve public-key encryption and decryption. Nothing in this section
is needed to complete the pset.
We want to encrypt messages that are strings of characters, but
the RSA system does not work with characters, but with integers. To
138 PROGRAMMING WELL
n=pq
m = (p − 1)(q − 1)
E (s ) = s e (mod n )
D (s ) = s d (mod n )
HANDLING ANOMALOUS CONDITIONS 139
D (E (s )) = (s e )d (mod n )
= s d e (mod n )
= s 1+km (mod n )
= s (s m )k (mod n )
= s 1k (mod n )
= s (mod n )
=s
For the last step to hold, the integer s representing the plaintext
must be less than n. That’s why we break the message up into chunks.
Also, this only works if s is relatively prime to n, that is, it has no fac-
tors in common with n other than 1. If n is the product of two large
primes, then all but negligibly few messages s < n satisfy this property.
If by some freak chance s and n turned out not to be relatively prime,
then the code would be broken; but the chances of this happening by
accident are insignificantly small.
In summary, to use the RSA system:
4. Publish the pair (n, e ) as your public key, but keep d , p, and q
secret.
How secure is RSA? At present, the only known way to obtain d from
e and n is to factor n into its prime factors p and q, then compute m
and proceed as above. But despite centuries of effort by number theo-
rists, factoring large integers efficiently is still an open problem. Until
someone comes up with an efficient way to factor numbers, or dis-
covers some other way to compute d from e and n, the cryptosystem
appears to be secure for large numbers n.
11
Algebraic data types
Data types can be divided into the atomic types (with atomic type
constructors like int and bool) and the composite types (with parame-
terized type constructors like ⟨⟩ * ⟨⟩ , ⟨⟩ list, and ⟨⟩ option).
What is common to all of the built-in composite types introduced
so far1 is that they allow building data structures through the combina- 1
The exception is the composite type
of functions. Functions are the rare
tion of just two methods.
case of a composite type in OCaml not
structured as an algebraic data type as
1. Conjunction: Multiple components can be conjoined to form a defined below.
composite value containing all of the components.
For instance, values of pair type, int * float say, are formed as
the conjunction of two components, the first component an int
and the second a float.
# type base = G | C | A | T ;;
type base = G | C | A | T
arguments (uncurried), one for the first base in the sequence and one
for the rest of the dna sequence.4 4
There is a subtle distinction concern-
ing when type constructors take a single
# type dna = tuple argument or multiple arguments
# | Nil written with tuple notation. For the
# | Cons of base * dna ;; most part, the issue can be ignored,
type dna = Nil | Cons of base * dna so long as the type definition doesn’t
place the argument sequence within
The Cons constructor takes two arguments (using tuple notation), the parentheses. For the curious, see the
“Note on tupled constructors” in the
first of type base and the second of type dna. It thus serves to conjoin a OCaml documentation.
base element and another dna sequence.
Having defined this new type, we can construct values of that type:
# let seq = Cons (A, Cons (G, Cons (T, Cons (C, Nil)))) ;;
val seq : dna = Cons (A, Cons (G, Cons (T, Cons (C, Nil))))
# complement seq ;;
- : dna = Cons (T, Cons (C, Cons (A, Cons (G, Nil))))
The dna type looks for all the world just like the list type built into
OCaml, except for the fact that its elements are always of type base.
6
We name the type bool_ so as not to
shadow the built-in type bool. Similarly
Indeed, our choice of names of the value constructors (Nil and Cons) for the underscore versions list_ and
emphasizes the connection. option_ below.
Value constructors in defined alge-
In fact, many of the built-in composite types can be implemented as
braic types are restricted to starting with
algebraic data types in this way. Boolean values are essentially a kind of capital letters in OCaml. The built-in
enumerated type, hence algebraic.6 type differs only in using lower case
constructors true and false.
# type bool_ = True | False ;;
type bool_ = True | False
144 PROGRAMMING WELL
Following the edict of irredundancy, we’d prefer not to write this same
code repeatedly, differing only in the type of the list elements. Fortu-
nately, variant type declarations can be polymorphic.
The variant type definitions in this chapter aren’t the first examples of
algebraic type definitions you’ve seen. In Section 7.4, we noted that
record types were user-defined types, defined with the type keyword,
as well.
A L G E B R A I C D ATA T Y P E S 145
Note the use of pattern-matching right in the header line, as well as the
use of field punning to simplify the pattern.
The evaluation of the query depends on its structure, so we’ll want
to match on that.
let rec eval ({title; words} : document)
(q : query)
: bool =
match q with
| Word word -> ...
| And (q1, q2) -> ...
| Or (q1, q2) -> ...
For the first variant, we merely check that the word occurs in the list of
words:
⟨pattern ⟩ as ⟨variable ⟩
A L G E B R A I C D ATA T Y P E S 147
for just such cases. Such a pattern both pattern matches against the
⟨pattern ⟩ as well as binding the ⟨variable ⟩ to the expression being
matched against as a whole. We use this technique both to provide
a name for the document as a whole (doc) and to extract its compo-
nents. (Once we have a variable doc for the document as a whole, we
no longer need to refer to title, so we use an anonymous variable
instead.)
let rec eval ({words; _} as doc : document)
(q : query)
: bool =
match q with
| Word word -> List.mem word words
| And (q1, q2) -> (eval doc q1) && (eval doc q2)
| Or (q1, q2) -> (eval doc q1) || (eval doc q2) ;;
That’s better. But we’re still calling eval doc four times on different
subqueries. We can abstract that function and reuse it; call it eval’:
let eval ({words; _} as doc : document)
(q : query)
: bool =
let rec eval' (q : query) : bool =
match q with
| Word word -> List.mem word words
| And (q1, q2) -> (eval' q1) && (eval' q2)
| Or (q1, q2) -> (eval' q1) || (eval' q2) in
... ;;
There’s an important idea hidden here, which follows from the scoping
rules of OCaml. Because the eval’ definition falls within the scope
of the definition of eval and the associated variables words and q,
those variables are available in the body of the eval’ definition. And in
fact, we make use of that fact by referring to words in the first pattern-
match. (The outer q is actually shadowed by the inner q, so it isn’t
referred to in the body of the eval’ definition. The occurrence of q in
the match q is a reference to the q argument of eval’.)
Now that we have eval’ defined it suffices to call it on the main
query and let the recursion do the rest. At this point, however, the
alternative variable name doc is no longer referenced, and can be
eliminated.
# let eval ({words; _} : document)
# (q : query)
# : bool =
# let rec eval' (q : query) : bool =
# match q with
# | Word word -> List.mem word words
# | And (q1, q2) -> (eval' q1) && (eval' q2)
# | Or (q1, q2) -> (eval' q1) || (eval' q2) in
# eval' q ;;
val eval : document -> query -> bool = <fun>
148 PROGRAMMING WELL
Let’s try it on some sample queries. We’ll use the first line of The Great
Gatsby.
We start with the docs, filter them with a function that applies eval to
select only those that satisfy the query, and then map a function over
them to extract their titles.
From a readability perspective, it is unfortunate that the description
of what the code is doing – start with the corpus, then filter, then map
– is “inside out” with respect to how the code reads. This follows from
the fact that in OCaml, functions come before their arguments in
applications, whereas in this case, we like to think about a data object
followed by a set of functions that are applied to it. A language with
backwards application would be able to structure the code in the more
readable manner.
Happily, the Stdlib module provides a B A C K WA R D S A P P L I C AT I O N
infix operator |> for just such occasions.
# succ 3 ;;
- : int = 4
# 3 |> succ ;; (* start with 3; increment *)
- : int = 4
# 3 |> succ (* start with 3; increment; ... *)
A L G E B R A I C D ATA T Y P E S 149
Exercise 95
What do you expect the type of |> is?
Exercise 96
How could you define the backwards application operator |> as user code?
Taking advantage of the backwards application operator can make
the code considerably more readable. Instead of
docs
|> List.filter (fun doc -> (eval doc q))
Then we can map the title extraction function over the result:
docs
|> List.filter (fun doc -> (eval doc q))
|> List.map (fun doc -> doc.title)
Edict of prevention:
12
An idiosyncrasy of OCaml requires
Make the illegal inexpressible. that the dictionary type be defined in
stages in this way, rather than all at once
We’ve seen this idea before in the small. It’s the basis of type checking as
itself, which allows the use of certain values only with functions that # type ('key, 'value) dictionary =
are appropriate to apply to them – integers with integer functions, # { key : 'key; value : 'value } list ;;
Line 2, characters 31-35:
booleans with boolean functions – preventing all other uses. In a 2 | { key : 'key; value : 'value } list ;;
strongly typed language like OCaml, illegal operations, like applying ^^^^
Error: Syntax error
an integer function to a boolean value, simply can’t be expressed as
The use of and to combine multiple type
valid well-typed code.
definitions into a single simultaneous
The edict of prevention11 challenges us to find an alternative struc- definition isn’t required here, but is
ture in which this kind of mismatch between the keys and values can’t when the type definitions are mutually
recursive.
occur. Such a structure may already have occurred to you. Instead
of thinking of a dictionary as a pair of lists of keys and values, we can
think of it as a list of pairs of keys and values.12
# type ('key, 'value) dict_entry =
# { key : 'key; value : 'value }
152 PROGRAMMING WELL
The type system will now guarantee that every dictionary is a list
whose elements each have a key and a value. A dictionary with un-
equal numbers of keys and values is not even expressible. The lookup
function can still recur through the pairs, looking for the match:
Problem 98
What is an appropriate type for a function better that determines which of two cards is
“better” in the context of mini-poker, returning true if and only if the first card is better
than the second?
Problem 99
Provide a definition of the function better.
(We’ll take this to define the abstract syntax of the language. Concrete
syntax notions like precedence and associativity of the operators and
parentheses for disambiguating structure will be left implicit in the
usual way.)
We can define a type for abstract syntax trees for these arithmetic
expressions as an algebraic data type. The definition follows the gram-
mar almost trivially, one variant for each line of the grammar.
# type expr =
# | Int of int
# | Plus of expr * expr
# | Minus of expr * expr
# | Times of expr * expr
# | Div of expr * expr
# | Neg of expr ;;
type expr =
Int of int
| Plus of expr * expr
| Minus of expr * expr
154 PROGRAMMING WELL
We can test the evaluator with examples like the one above.
# (3 + 4) * ~- 5 ;;
- : int = -35
# 42 ;;
- : int = 42
# 5 / 0 ;;
Exception: Division_by_zero.
Exercise 100
Define a version of eval that implements a different semantics for the expression
language, for instance, by rounding rather than truncating integer divisions.
Exercise 101
Define a function e2s : expr -> string that returns a string that represents the fully
parenthesized concrete syntax for the argument expression. For instance,
# e2s (Times (Plus (Int 3, Int 4), Neg (Int 5))) ;;
- : string = "((3 + 4) * (~- 5))"
# e2s (Int 42) ;;
- : string = "42"
# e2s (Div (Int 5, Int 0)) ;;
- : string = "(5 / 0)"
The opposite process, recovering abstract syntax from concrete syntax, is called parsing.
More on this in the final project (Chapter 21).
(a)
11.5 Example: Binary trees
Trees are a class of data structures that store values of a certain type
in a hierarchically structured manner. They constitute a fundamen-
tal data structure, second only perhaps to lists in their repurposing
flexibility. Indeed, the arithmetic expressions of Section 11.4 are a
kind of tree structure, as are the binary search trees mentioned in
Section 11.6.1.
In this section, we concentrate on a certain kind of polymorphic
B I N A RY T R E E , a kind of tree whose nodes have distinct left and right
subtrees, possibly empty. Some examples can be seen in Figure 11.3.
(b)
A binary tree can be an empty tree (depicted with a bullet symbol (●)
in the diagrams), or a node that stores a single value (of type ’a, say)
along with two subtrees, referred to as the left and right subtrees.
A polymorphic binary tree type can thus be defined by the following
algebraic data type definition:
# let int_bintree =
# Node (16, Node (93, Empty, Empty),
# Node (3, Node (42, Empty, Empty),
# Empty)) ;;
val int_bintree : int bintree =
Node (16, Node (93, Empty, Empty),
Node (3, Node (42, Empty, Empty), Empty))
Exercise 102
Construct a value str_bintree of type string bintree that encodes the tree of
Figure 11.3(b).
Now let’s write a function to sum up all of the elements stored in an
integer tree. The natural approach to carrying out the function is to
follow the recursive structure of its tree argument.
Exercise 103
Define a function preorder of type ’a bintree -> ’a list that returns a list of all of
the values stored in a tree in P R E O R D E R , that is, placing values stored at a node before
the values in the left subtree, in turn before the values in the right subtree. For instance,
# preorder int_bintree ;;
- : int list = [16; 93; 3; 42]
arguments: (i) the value to use for empty trees, (ii) the function to ap-
ply at nodes to the value stored at the node and the values associated
with the two subtrees, along with (iii) a tree to walk; it carries out the
recursive process on that tree. Since this is a kind of “fold” operation
over binary trees, we’ll name the function foldbt.
Exercise 104
What is the appropriate type for the function foldbt just described?
Exercise 105
Define the function foldbt just described.
Exercise 106
Redefine the function sum_bintree using foldbt.
Exercise 107
Redefine the function preorder using foldbt.
Exercise 108
Define a function find : ’a bintree -> ’a -> bool in terms of foldbt, such that
find t v is true just in case the value v is found somewhere in the tree t.
# find int_bintree 3 ;;
- : bool = true
# find int_bintree 7 ;;
- : bool = false
In this lab, you will define and use algebraic data types to model res-
idential address information, and work with a specific type of binary
tree, the B I N A RY S E A R C H T R E E , which allows for efficient storage and
search of ordered information. A particular application is the use of
Figure 11.4: Saul Gorn with the
G O R N A D D R E S S E S , named after the early computer pioneer Saul Gorn
UNIVAC-1 computer at the Univer-
of University of Pennsylvania (Figure 11.4), who invented the tech- sity of Pennsylvania, 1950s.
nique.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
Exercise 8.) The earliest computing devices were used to calculate nu-
merically. Charles Babbage envisioned his analytical engine as a device
for calculating numeric tables, and Ada Lovelace’s famous program for
Babbage’s analytical engine numerically calculated Bernoulli numbers.
But Lovelace (Figure 11.5) was perhaps the first computer scientist
to have the revolutionary idea that computers could be used for much
more than numerical calculations.
The operating mechanism. . . might act upon other things besides num-
ber, were objects found whose mutual fundamental relations could be
expressed by those of the abstract science of operations, and which
should be also susceptible of adaptations to the action of the operating
notation and mechanism of the engine. Supposing, for instance, that
the fundamental relations of pitched sounds in the science of harmony
and of musical composition were susceptible of such expression and
adaptations, the engine might compose elaborate and scientific pieces Figure 11.5: A rare daguerrotype of Ada
Lovelace (Augusta Ada King, Countess
of music of any degree of complexity or extent. (Menabrea and Lovelace,
of Lovelace, 1815–1852) by Antoine
1843, page 694) Claudet, taken c. 1843, around the
time she was engaged in writing her
One of the applications of the power of computers to transcend nu- notes on the Babbage analytical engine.
merical calculation, which Lovelace immediately saw, was to engage in (Menabrea and Lovelace, 1843)
(* Unary operators. *)
type unop = Sin | Cos | Ln | Neg ;;
(* Expressions *)
type expression =
| Num of float
| Var
| Binop of binop * expression * expression
| Unop of unop * expression ;;
You can think of the data objects of this expression type as defin-
ing trees where nodes are the type constructors and the children of
each node are the specific operator to use and the arguments of that
constructor. These are just the abstract syntax trees of Section 3.3.
Although numeric expressions frequently make use of parentheses
– and sometimes necessarily so, as in the case of the expression (x +
3)(x − 1) – the expression data type definition has no provision for
parenthesization. Why isn’t that needed? It might be helpful to think
about how this example would be represented.
# parse ("5+x*8") ;;
- : ExpressionLibrary.expression =
Binop (Add, Num 5., Binop (Mul, Var, Num 8.))
# to_string_smart exp ;;
- : string = "x^2.+sin(x/5.)"
• checkexp : Takes a string and a value and prints the results of call-
ing every function to be tested except find_zero.
( f (g (x )))′ = f ′ (g (x )) ⋅ g ′ (x )
Using the chain rule, we can write the derivatives for the other func-
tions in our language, as shown in Figure 11.8.13 13
If the kinds of notation used here
are unfamiliar, the discussion in Sec-
tion A.1.3 may be helpful.
( f (x ) − g (x ))′ = f ′ (x ) − g ′ (x )
( f (x ) ⋅ g (x ))′ = f ′ (x ) ⋅ g (x ) + f (x ) ⋅ g ′ (x )
f (x ) ( f ′ (x ) ⋅ g (x ) − f (x ) ⋅ g ′ (x ))
′
( ) =
g (x ) g (x )2
(sin f (x ))′ = f ′ (x ) ⋅ cos f (x )
(cos f (x ))′ = f ′ (x ) ⋅ ~ sin f (x )
f ′ (x )
(ln f (x ))′ =
f (x )
( f (x ) ) = h ⋅ f ′ (x ) ⋅ f (x )h −1
h ′
is useful to treat them separately, because when the first case applies,
the second case produces unnecessarily complicated expressions.
Your task is to implement the derivative function whose type is
expression -> expression. The result of your function must be cor-
rect, but need not be expressed in the simplest form. Take advantage of
this in order to keep the code in this part as short as possible.
Once you’ve implemented the derivative function, you should be
able to calculate the symbolic derivative of various functions. Here’s an
example, calculating the derivative of x 2 + 5:
# to_string_smart (derivative (parse "x^2 + 5")) ;;
- : string = "2.*1.*x^(2.-1.)+0."
The result generated, 2 ⋅ 1 ⋅ x 2−1 + 0, isn’t in its simplest form, but it does
correctly capture the derivative, 2 ⋅ x.
To make your task easier, we have provided an outline of the func-
tion with many of the cases already filled in. We also provide a func-
tion, checkexp, which checks the functions you write in Problems
1–3 for a given input. The portions of the function that require your
attention currently read failwith "not implemented".
Note that there are cases where Newton’s method will fail to pro-
duce a zero, such as for the function x 1/3 . You are not responsible for
finding a zero in those cases, but just for the correct implementation of
Newton’s method.
The algebraic data types introduced in the last chapter are an expres-
sive tool for defining sophisticated data structures. But with great
power comes great responsibility.
As an example, consider one of the most fundamental of all data
structures, the QU E U E . A queue is a collection of elements that admits
of operations like creating an empty queue, adding elements one by
one (called E N QU E U E I N G ), and removing them one-by-one (called
D E QU E U I N G ), where crucially the first element enqueued is the first to
be dequeued. The common terminology for this regimen is F I R S T- I N -
F I R S T- O U T or FIFO.
We can provide a concrete implementation of the queue data type
using the list data type, along with functions for enqueueing and de-
queueing. An empty queue will be implemented as the empty list, with
non-empty queues storing elements in order of their enqueueing, so
newly enqueued elements are added at the end of the list.
# let q = empty_queue
# |> enqueue 1 (* enqueue 1, 2, and 4 *)
# |> enqueue 2
# |> enqueue 4 ;;
val q : int list = [1; 2; 4]
# let next, q = dequeue q ;; (* dequeue 1 *)
val next : int = 1
val q : int list = [2; 4]
# let next, q = dequeue q ;; (* dequeue 2 *)
val next : int = 2
val q : int list = [4]
# let next, q = dequeue q ;; (* dequeue 4 *)
val next : int = 4
val q : int list = []
# let q = empty_queue
# |> enqueue 1 (* enqueue 1, 2, and 4 *)
# |> enqueue 2
# |> enqueue 4
# |> List.rev ;; (* yikes! *)
val q : int list = [4; 2; 1]
# let next, q = dequeue q ;; (* dequeue 4 *)
val next : int = 4
val q : int list = [2; 1]
# let next, q = dequeue q ;; (* dequeue 2 *)
val next : int = 2
val q : int list = [1]
# let next, q = dequeue q ;; (* dequeue 1 *)
val next : int = 1
val q : int list = []
other operation appropriate for lists but not queues. What we need is
the ability to enforce restraint on the operations applicable to a data
structure so as to preserve the invariants.
An analogy: The lights and heating in hotel rooms are intended to
be on when the room is occupied, but they should be lowered when
the room is empty. We can think of this as an invariant: If the room is
unoccupied, the lights and heating are off. One approach to increasing
compliance with this invariant is through documentation, placing a (a) (b)
sign at the door “Please turn off the lights when you leave.” But many Figure 12.1: Two approaches to pre-
serving the invariant that the lights
hotels now use a key card switch, a receptacle near the door in which are off when the room is vacant: (a) an
you insert the key card for the hotel room when you enter, in order exhortation documenting the invariant;
(b) a key card switch that disables the
to enable the lights and heating. (See Figure 12.1.) Since you have
lights when the key is removed.
to bring your key card with you when you leave the room, thereby
disabling the lights and heating, there is literally no way to violate
the invariant. The state of California estimates that widespread use
of hotel key card switches saves tens of millions of dollars per year
(California Utilities Statewide Codes and Standards Team, 2011, page
6). Preventing violation of an invariant beats documenting it.
We’ve seen this idea of avoiding illegal states before in the edict of
prevention. But in the queue example, type checking doesn’t stop us
from representing a bad state, and simple alternative representations
for queues that prevent inappropriate operations don’t come to mind.
We need a way to implement new data types and operations such that
the values of those types can only be used with the intended opera-
tions. We can’t make the bad queues unrepresentable, but perhaps we
can make them inexpressible, which should be sufficient for gaining
the benefit of the edict of prevention.
The key idea is to provide an A B S T R A C T D ATA T Y P E (ADT), a data
type definition that provides not only a concrete I M P L E M E N TAT I O N
of the data type values and operations on them, but also enforces that
only those operations can be applied, making it impossible to express
the application of other operations. This influential idea, the basis for
modular programming, was pioneered by Barbara Liskov (Figure 12.2
in her CLU programming language.
The allowed operations are specified in a S I G N AT U R E ; no other
Figure 12.2: The idea of abstract data
aspects of the implementation of the data type can be seen other types – grouping some functionality
than those specified by the signature. Users of the abstract data type over types and hiding the implementa-
tion of that functionality behind a strict
can avail themselves of the functionality specified in the signature,
interface – is due to computer scientist
while remaining oblivious of the particularities of the implementa- Barbara Liskov, and is first seen in her
tion. The signature specifies an interface to using the data structure, influential CLU programming language
from 1974. Her work on data abstraction
which serves as an A B S T R A C T I O N B A R R I E R ; only the aspects of the and object-oriented programming led
implementation specified in the signature may be made use of. to her being awarded the 2008 Turing
Award, computer science’s highest
The idea of hiding aspects of the implementation from those who
honor.
170 PROGRAMMING WELL
Edict of compartmentalization:
Limit information to those with a need to know.
In the case of the queue abstract data type, all that users of the
implementation have a need to know is the types for the operations
involving queues, viz., the creation of queues and the enqueueing and
dequeueing of elements; that’s all the signature should specify. The im-
plementation may be in terms of lists (or any of a wide variety of other
methods) but the users of the abstract data type should not be able to
avail themselves of the further aspects of the implementation. By pre-
venting them from using aspects of the implementation, the invariants
implicit in the signature can be maintained. A further advantage of
hiding the details of the implementation of a data structure behind the
abstraction barrier (in addition to making illegal operations inexpress-
ible) is that it becomes possible to modify the implementation without
affecting its use. This aspect of abstract data types is tremendously
powerful.
We’ve seen other applications of the edict of compartmentaliza-
tion before, for instance, in the use of helper functions local to (and
therefore only accessible to) a function being defined. The alternative,
defining the helper function globally could lead to unintended use of
and reliance on that function, which had been intended only for its
more focused purpose.
12.1 Modules
Just as values can be named using the let construct, modules can
be named using the module construct:
module ⟨modulename ⟩ =
⟨moduledefinition ⟩
Exercise 109
Define a different implementation of integer queues as int lists where the elements
are kept with older elements farther from the head of the list. What are the advantages
and disadvantages of this implementation?
Components of a module are referenced using the already fa-
miliar notation of prefixing the module name and a dot before the
component. We’ve seen this already in examples like List.nth or
Str.split. Similarly, users of the IntQueue module can refer to
172 PROGRAMMING WELL
# let q = IntQueue.empty_queue
# |> IntQueue.enqueue 1 (* enqueue 1, 2, and 4 *)
# |> IntQueue.enqueue 2
# |> IntQueue.enqueue 4 ;;
val q : IntQueue.int_queue = [1; 2; 4]
# open IntQueue ;;
# let q = empty_queue
# |> enqueue 1 (* enqueue 1, 2, and 4 *)
# |> enqueue 2
# |> enqueue 4 ;;
val q : IntQueue.int_queue = [1; 2; 4]
# let next, q = dequeue q ;; (* dequeue 1 *)
val next : int = 1
val q : IntQueue.int_queue = [2; 4]
# let next, q = dequeue q ;; (* dequeue 2 *)
val next : int = 2
val q : IntQueue.int_queue = [4] 2
A useful technique to simplify access
# let next, q = dequeue q ;; (* dequeue 4 *) to a module without opening it (and
val next : int = 4 thereby shadowing any existing names)
val q : IntQueue.int_queue = [] is to provide a short alternative name for
the module.
of the components, that is, how they are used – from what the imple-
menter or developer sees – the module implementation, including the
concrete types and values for the components, that is, how they are
implemented.
The notation for specifying signatures is similar to that for modules,
except for the use of sig instead of struct; and naming signatures is
like naming modules with the addition of the evocative type keyword.
The signature provides a full listing of all the aspects of a module that
are visible to users of the module. In particular, the module provides a
type called int_queue, but since the concrete implementation of that
type is not provided in the signature, it is unavailable to users of mod-
ules satisfying the signature. The signature states that the module must
provide a value empty_queue but what the concrete implementation of
that value is is again hidden. And so on.
Notice that where the module implementation defines named
values using the let construct, the signature uses the val construct,
which provides a name and a type, but no definition of what is named.
Extending the analogy between signatures and types further, we can
specify that a module satisfies and is constrained by a signature with a
notation almost identical to that constraining a value to a certain type.
# open IntQueue ;;
# let q = empty_queue
# |> enqueue 1 (* enqueue 1, 2, and 4 *)
# |> enqueue 2
# |> enqueue 4 ;;
val q : IntQueue.int_queue = <abstr>
# List.rev q ;;
Line 1, characters 9-10:
1 | List.rev q ;;
^
Error: This expression has type IntQueue.int_queue
but an expression was expected of type 'a list
What happens when a module defines more components than its sig-
nature provides for? As a trivial example, we will define an O R D E R E D
TYPE as a type that has an associated comparison function that pro-
vides an ordering on elements of the type. The definition of such a
module provides for these two components: a type, call it t, and a
function that takes two elements x and y of type t and returns an inte-
ger indicating the ordering of the two, -1 if x is smaller, +1 if x is larger,
and 0 if the two are equal in the ordering.4 4
We use this arcane approach for
the compare function to mimic the
This specification of what constitutes an ordered type can be cap-
Stdlib.compare library function.
tured in a signature ORDERED_TYPE: Frankly, a better approach would be
to take the result of the comparison to
# module type ORDERED_TYPE = be a value in an enumerated type type
# sig order = Less | Equal | Greater.
# type t
# val compare : t -> t -> int
# end ;;
module type ORDERED_TYPE = sig type t val compare : t -> t -> int
end
# module PointOrderedType =
# struct
# type t = float * float
# let norm (x, y) =
# x ** 2. +. y ** 2.
# let compare p1 p2 =
# Stdlib.compare (norm p1) (norm p2)
# end ;;
module PointOrderedType :
sig
type t = float * float
val norm : float * float -> float
176 PROGRAMMING WELL
We can make use of the module to see how this ordering works on
some examples.
# let open PointOrderedType in
# compare (1., 1.) (5., 0.),
# compare (1., 1.) (-1., -1.),
# compare (1., 1.) (0., 1.1) ;;
- : int * int * int = (-1, 0, 1)
# PointOrderedType.norm ;;
- : float * float -> float = <fun>
# PointOrderedType.norm (1., 1.) ;;
- : float = 2.
In general, only the aspects of a module consistent with its signature are
visible outside of its implementation to users of the module. All other
aspects are hidden behind the abstraction barrier. In particular, the
norm function is not available, and the identity of the type t is hidden
as well. We can tell, because we no longer can compare two points.
# PointOrderedType.compare (1., 1.) (5., 0.) ;;
Line 1, characters 25-33:
1 | PointOrderedType.compare (1., 1.) (5., 0.) ;;
^^^^^^^^
Error: This expression has type 'a * 'b
but an expression was expected of type PointOrderedType.t
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 177
# open Queue ;;
# let intq = empty_queue
# |> enqueue 1
# |> enqueue 2 ;;
val intq : int Queue.queue = <abstr>
# let boolq = empty_queue
# |> enqueue true
# |> enqueue false ;;
val boolq : bool Queue.queue = <abstr>
# dequeue intq ;;
- : int * int Queue.queue = (1, <abstr>)
# dequeue boolq ;;
- : bool * bool Queue.queue = (true, <abstr>)
Exercise 110
In Section 11.3, we provided a data type for dictionaries that makes sure that the keys
and values match up properly. We noted, however, that nothing prevents building a
dictionary with multiple occurrences of the same key.
Define a dictionary module signature and implementation that implements dictio-
naries using the type from Section 11.3, and provides a function
One of the primary advantages of using abstract data types (as op-
posed to concrete data structures) is that by hiding the data type im-
plementations, the implementations can be changed without affecting
users of the data types.
Recall the query type from Section 11.2.
# type query =
# | Word of string
# | And of query * query
# | Or of query * query ;;
type query = Word of string | And of query * query | Or of query *
query
Using a reverse index, the code for evaluating a query is quite simple:
Of course, we’ll need code for the intersection of two lists. Here’s an
approach, in which the lists are kept sorted to facilitate finding dupli-
cates:
like union and intersection. The index module, call it Index would
provide a lookup function. The eval function using these modules
then becomes
This is much nicer. It says what the code does at the right level of ab-
straction, in terms of high-level operations like dictionary lookup, or
set intersection and union. It remains silent, as it should, about exactly
how those operations are implemented.
Now we’ll need module definitions for Index and StringSet. We
start with StringSet first, and in particular, its module signature,
since this specifies how the module can be used.
A string set module needs to provide some operations for creating and
manipulating the sets. The requirements can be specified in a module
signature. Here’s a first cut:
• and so forth.
From the point of view of the users (callers) of this abstract data
type, this is all they need to know: The name of the type and the func-
tions that apply to values of that type.
To drive this point home, we’ll make use of an implementation
(StringSet) of this abstract data type before even looking at the im-
plementing code.
# let s = StringSet.add "c"
# (StringSet.add "b"
# (StringSet.add "a" StringSet.empty)) ;;
val s : StringSet.set = <abstr>
Note that the string set we’ve called s is of the abstract type
StringSet.set and the particulars of the value implementing the
set are hidden from us as <abstr>.
The types, values, and functions provided in the signature are nor-
mal OCaml objects that interact with the rest of the language as usual.
We can still avail ourselves of the rest of OCaml. For instance, we can
clean up the definition of s using reverse application and a local open:
# let s =
# let open StringSet in
# empty
# |> add "a"
# |> add "b"
6
You’ll notice that we don’t bother
# |> add "c" ;;
adding types to the definitions of the
val s : StringSet.set = <abstr>
values in this module implementation.
Since the signature already provided
Other operations work as well.
explicit types (satisfying the edict of
# StringSet.member "a" s ;; intention), OCaml can verify that the
- : bool = true implementation respects those types.
# StringSet.member "d" s ;; Nonetheless, it can sometimes be useful
to provide further typing information in
- : bool = false
a module implementation.
Of course, the ADT must have an actual implementation for it to work.
We’ve just been assuming one, but we can provide a possible imple-
mentation (the one we’ve been using as it turns out), obeying the
specific signature we just defined.6
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 183
And it’s a good thing too, because if we could have added the "b"
to the list, suddenly, the list doesn’t obey the invariant required by
the implementation that there be no duplicates. But because of the
abstraction barrier, there’s no way for a user of the module to break the
invariant, so long as the implementation maintains it.
Because the sets are implemented as unsorted lists, when taking the
union of two sets set1 and set2, we must traverse the entirety of the
set2 list once for each element of set1. For small sets, this is not likely
to be problematic, and worrying about this inefficiency may well be a
premature effort at optimization.7 But for a set implementation likely 7
In the introduction to Chapter 14 you’ll
learn that “premature optimization is
to be used widely and on very large sets, it may be useful to address the
the root of all evil.”
issue.
A better alternative from an efficiency point of view is to implement
sets as sorted lists. This requires a bit more work in adding elements
to a set to place them in the right order, but saves effort for union and
184 PROGRAMMING WELL
# let s =
# let open StringSet in
# empty
# |> add "a"
# |> add "b"
# |> add "c" ;;
val s : StringSet.set = <abstr>
# StringSet.member "a" s ;;
- : bool = true
# StringSet.member "d" s ;;
- : bool = false
And here’s the payoff. Even though we’ve completely changed the
implementation of string sets, even using a data structure obeying a
different invariant, the code for using string sets changes not at all.
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 185
# StringSet.empty ;;
- : StringSet.set = <abstr>
What’s the problem? It turns out that the abstraction barrier provided
by the SET signature is doing exactly what it should. The implementa-
tion promises to deliver something that satisfies and reveals SET. And
that’s all. The SET signature reveals types set and element, not string
list and string. Viewed from within the implementation, the types
element and string are the same. But from outside the module im-
plementation, only element is available, leading to the type mismatch
with string.
This is a case in which the abstraction barrier is too strict. (We
saw this before in Section 12.3.) We do want to allow the user of the
module to have access to the implementation of the element type, if
only so that module users can provide elements of that type. Rather
than using the too abstract SET signature, we can define slightly less
abstract signatures using S H A R I N G C O N S T R A I N T S , which augment
a signature with one or more type equalities across the abstraction
barrier, identifying abstract types within the signature (element) with
implementations of those types accessible outside the implementation
(string).8 8
Notice how in printing out the result
of defining the new STRING_SET signa-
# module type STRING_SET = SET with type element = string ;; ture, OCaml specifies that the type of
module type STRING_SET = elements is string. Compare this with
sig the version above without the sharing
type set constraint.
type element = string This example requires only a single
sharing constraint, but multiple con-
val empty : set
straints can be useful as well. They are
val is_empty : set -> bool
combined with the and keyword, for
val add : element -> set -> set
example, the pair of sharing constraints
val union : set -> set -> set with type key = D.key and type
val intersection : set -> set -> set value = D.value used in the definition
val member : element -> set -> bool of the MakeOrderedDict module in
end Section 12.6.
# StringSet.empty ;;
- : StringSet.set = <abstr>
# StringSet.member "a" StringSet.empty ;;
- : bool = false
# let s =
# let open StringSet in
# empty
# |> add "first"
# |> add "second"
# |> add "third" ;;
val s : StringSet.set = <abstr>
# StringSet.union s s ;;
- : StringSet.set = <abstr>
# StringSet.member "a" s ;;
- : bool = false
some values of that type or functions over the type, or even multiple
types.
If only we had a way of packaging up some types and related values
and functions. But we do have such a way: the module system itself. In
effect, what we need is something akin to a function that takes as ar-
gument a module defining the parameters of the implementation and
returns the desired module. We call these “functions” from modules to
modules F U N C T O R S .
We can use the StringSet and IntSet implementations as the
basis for a functor MakeOrderedSet, which takes a module as argu-
ment to provide the element type and returns a module satisfying the
SET signature. As described above, the argument module should have
a type (call it t) and a way of comparing elements of the type (call it
compare). We’ll have the compare function take two elements of type
t and return an integer specifying whether the first integer is less than
(-1), equal to (0), or greater than (1) the second integer.
You may recognize this signature. It’s the ORDERED_TYPE signature
from Section 12.3, repeated here for reference.
# module type ORDERED_TYPE =
# sig
# type t
# val compare : t -> t -> int
# end ;;
module type ORDERED_TYPE = sig type t val compare : t -> t -> int
end
# | hd :: tl ->
# (match Elements.compare elt hd with
# | 0
(* equal *) -> s
# | -1 (* less *) -> elt :: s
# | _ (* greater *) -> hd :: add elt tl)
# let union = List.fold_right add
# let rec intersection set1 set2 =
# match set1, set2 with
# | [], _ -> []
# | _, [] -> []
# | h1::t1, h2::t2 ->
# (match Elements.compare h1 h2 with
# | 0 (* equal *) -> h1 :: intersection t1 t2
# | -1 (* less *) -> intersection t1 set2
# | _ (* greater *) -> intersection set1 t2)
# end ;;
module MakeOrderedSet : functor (Elements : ORDERED_TYPE) -> SET
But this won’t do. The returned module satisfies SET, but we’ve
already seen how this is too strong a requirement. The solution is the
same as before, use sharing constraints to allow access to the element
type.
# module MakeOrderedSet (Elements : ORDERED_TYPE)
# : (SET with type element = Elements.t) =
# struct
# type element = Elements.t
# type set = element list
# let empty = []
# let is_empty s = (s = [])
# let rec member elt s =
# match s with
# | [] -> false
# | hd :: tl ->
# (match Elements.compare elt hd with
# | 0
(* equal *) -> true
# | -1 (* less *) -> false
# | _ (* greater *) -> member elt tl)
# let rec add elt s =
# match s with
# | [] -> [elt]
# | hd :: tl ->
# (match Elements.compare elt hd with
# | 0
(* equal *) -> s
# | -1 (* less *) -> elt :: s
# | _ (* greater *) -> hd :: add elt tl)
# let union = List.fold_right add
# let rec intersection set1 set2 =
# match set1, set2 with
# | [], _ -> []
# | _, [] -> []
# | h1::t1, h2::t2 ->
# (match Elements.compare h1 h2 with
# | 0 (* equal *) -> h1 :: intersection t1 t2
192 PROGRAMMING WELL
Here we finally have a functor that can generate a set module for any
type. Let’s generate a few, starting with a string set module, which we
can generate by applying the MakeOrderedSet functor to a module
satisfying ORDERED_TYPE linking the string type to an appropriate
ordering function (here, the default Stdlib.compare function).
# module StringSet = MakeOrderedSet
# (struct
# type t = string
# let compare = compare
# end) ;;
module StringSet :
sig
type set
type element = string
val empty : set
val is_empty : set -> bool
val add : element -> set -> set
val union : set -> set -> set
val intersection : set -> set -> set
val member : element -> set -> bool
end
It works as expected:
# let s =
# let open StringSet in
# empty
# |> add "first"
# |> add "second"
# |> add "third" ;;
val s : StringSet.set = <abstr>
# StringSet.union s s ;;
- : StringSet.set = <abstr>
# StringSet.member "a" s ;;
- : bool = false
We’ll want a functor that builds dictionaries for all kinds of keys
and values. In order to make sure we can compare the keys properly,
including ordering them, we’ll need a comparison function for keys as
well. While we’re at it, we might as well use a nicer convention for the
comparison function, which will return a value of type
type order = Less | Equal | Greater ;;
The argument to the functor should thus satisfy the following signa-
ture:
# module type DICT_ARG =
# sig
# type key
# type value
# (* We need to reveal the order type so users of the
# module can match against it to implement compare *)
# type order = Less | Equal | Greater
# (* Comparison function on keys compares two elements
# and returns their order *)
# val compare : key -> key -> order
# end ;;
module type DICT_ARG =
sig
type key
type value
type order = Less | Equal | Greater
val compare : key -> key -> order
end
key and value types and the ordering of keys. It allows access to the key
and value types via sharing constraints, so users of modules generated
by the functor can provide values of those types. This particular imple-
mentation of dictionaries is a simple list of key-value pairs, sorted by
unique keys.
module MakeOrderedDict :
functor (D : DICT_ARG) ->
sig
type key = D.key
type value = D.value
type dict
val empty : dict
val lookup : dict -> key -> value option
val member : dict -> key -> bool
val insert : dict -> key -> value -> dict
val remove : dict -> key -> dict
end
ation in a manner that is oblivious to, hence robust to any changes in,
the implementation of the sets and dictionaries. The code for eval can
be as specified before, and repeated here.
useful on occasions where the signature is quite short and will only be
used once, so retaining a name for it isn’t needed.
198 PROGRAMMING WELL
The facilities for generating set modules – including the SET signature
and MakeOrderedSet functor – might well be packaged up into a single
module themselves. A file set.ml providing such a module might look
like the following:
(* A Set Module *)
(*.......................................................
Set interface
*)
(*.......................................................
An implementation for elements of ordered type
*)
let empty = []
let is_empty s = (s = [])
let rec member elt s =
match s with
| [] -> false
| hd :: tl ->
let open Elements in
(* so that Elements.compare, Elements.Less,
etc. are in scope *)
match compare elt hd with
| Equal -> true
200 PROGRAMMING WELL
This file defines a module called set that enables usage like the
following, to define and use a StringSet module:
module StringSet =
let open Set in
MakeOrderedSet
(struct
type t = string
type order = Less | Equal | Greater
let compare s t = if s < t then Less
else if s = t then Equal
else Greater
end) ;;
let s = StringSet.create
|> StringSet.add "a"
|> StringSet.add "b"
|> StringSet.add "a" ;;
Data structures like sets and dictionaries are so generally useful that
you might think the language ought to provide them so that each indi-
vidual programmer doesn’t need to implement them. In fact, OCaml
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 201
We define here a signature for modules that deal with images and their
manipulation.
The pixels that make up an image are specified by the following signa-
ture:
Problem 111
We’d like to implement a functor named MakeImaging for generating implementations
of the IMAGING signature based on modules satisfying the PIXEL signature. How should
such a functor start? Give the header line of such beginning with the keyword module
and ending with the = struct....
Problem 112
Write code that uses the IntPixel module to define an imaging module called
IntImaging.
Problem 113
Write code to use the IntImaging module that you defined in Problem 112 to display a
100 by 100 pixel image where all of the pixels have the constant integer value 5000.
The possible relations between two intervals are depicted in Fig- Contains
ure 12.3. (For the interval arithmetic cognoscenti, we’ve left out
many details, such as whether intervals are open or closed; more Disjoint
Problem 117
The intersection of two intervals is only well-defined if the intervals are not disjoint. As-
sume that the DiscreteTimeInterval module has been opened, allowing you to make
use of everything in its signature. Now, define a function intersection : interval
-> interval -> interval option that takes two intervals and returns None if they are
disjoint and otherwise returns their intersection (embedded appropriately in the option
type).
Problem 118
Provide three different unit tests that would be useful in testing the correctness of the
DiscreteTimeInterval module.
The artist Alexander Calder (1898-1976) is well known for his distinc-
Figure 12.4: Alexander Calder’s
tive mobiles, sculptures with different shaped objects hung from a L’empennage (1953).
cascade of connecting metal bars. An example is given in Figure 12.11.
204 PROGRAMMING WELL
His mobiles are made with varying shapes at the ends of the con-
nectors – circles, ovals, fins. The exquisite balance of the mobiles
depends on the weights of the various components. In the next few
exercises of this problem, you will model the structure of mobiles as
binary trees such that one can determine if a Calder-like mobile design
is balanced or not. Let’s start with the objects at the ends of the con-
nectors. For our purposes, the important properties of an object will be
its shape and its weight (in arbitrary units; you can interpret them as
pounds).
Problem 119
Define a weight type consisting of a single floating point weight.
Problem 120
Define a shape type, a variant type that allows for three different shapes: circles, ovals,
and fins.
Problem 121
Define an object type that will be used to store information about the objects at the
ends of the connectors, in particular, their weight and their shape.
This module signature specifies separate types for the leaves of trees
and the internal nodes of trees, along with a type for the trees them-
selves; functions for constructing leaf and node trees; and a single
function to "walk" the tree. (We’ll come back to the walk function
later.) In addition to the signature for binary tree modules, we would
need a way of generating implementations of modules satisfying the
BINTREE signature, which we’ll do with a functor MakeBintree. The
MakeBinTree functor takes an argument module of type BINTREE_ARG
that packages up the particular types for the leaves and nodes, that is,
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 205
the types to use for leaft and nodet. The following module signature
will work:
module type BINTREE_ARG =
sig
type leaft
type nodet
end ;;
Problem 122
Write down the header of a definition of a functor named MakeBintree taking a
BINTREE_ARG argument, which generates modules satisfying the BINTREE signature.
Keep in mind the need for users of the functor-generated modules to access appropriate
aspects of the generated trees. (You don’t need to fill in the actual implementation of the
functor.)
Problem 126
What is the type of size?
206 PROGRAMMING WELL
Problem 127
Use the fact that the walk function is curried to give a slightly more concise definition for
size.
Problem 128
Use the walk function to implement a function shape_count : shape ->
Mobile.tree -> int that takes a shape and a mobile (in that order), and returns
the number of objects in the mobile that have that particular shape.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
The signature explicitly lists the types and values that any module
implementing this interface must define, as well as the exceptions
the implementation provides, and that functions in the interface may
raise.9 For a function like getmin, we could instead choose to return 9
Because of how OCaml handles excep-
tions, listing exceptions is optional, and
an ’a option, which would avoid the need for an exception. But you
you can’t indicate with code which func-
should get used to exceptions like these in modules, since OCaml mod- tions may cause which exceptions, but
ules tend to use them. Remember, functions are values, so functions it is good style to mention in a function’s
comments what exceptions it may raise
are also listed with the val keyword. and under what conditions.
The interface for ORDERED_COLLECTION_0 is not ideal. Consider the
following questions:
• Is ORDERED_COLLECTION_0 a type?
• How might a call to delete give you incorrect behavior for a cor-
rectly constructed tree?
For each node in a binary search tree, all values stored in its left subtree
are less than the value stored at the node, and all values stored in its
right subtree are greater than the values stored at the node.
What if there are multiple values in the tree, even distinct ones, that
are equal in the ordering? We’ll store all such elements together at a
single node, say as a list.
For instance, consider the following set of elements of type int *
string, gleaned from my personal bucket list:10 10
I won’t say which ones I’ve completed.
implementation, the elements in the queue are stored in a simple list in priority order.
This implementation is not ideal because either the take or the add operation is O (n )
complexity. (See Chapter 14.)
Problem 133
Implement TreeQueue, which is less naive than ListQueue (but still not ideal). In this
implementation of the PRIOQUEUE interface, the queue is stored as a binary search tree
using the BinSTree functor that you’ve already implemented.
• What is the worst case complexity of add and take for a TreeQueue?
Ordering invariant: The value stored at each node is smaller than all
values stored in the subtrees below the node.
(Ordered trees thus have the attractive property that the minimum
element is always stored at the root of the tree.)
In the skeleton code for the BinaryHeap functor in prioqueue.ml,
we have defined the tree type for implementing the binary heap,
which provides further clarification:
type tree =
| Leaf of elt
| OneBranch of elt * elt
| TwoBranch of balance * elt * tree * tree
The trees for use in forming binary heaps are of three sorts, corre-
sponding to the three variants in the type definition:
• Leaves store a single value of type elt (like the 17 node in Fig-
ure 12.7).
• Along the bottom edge of the tree, a tree with a single child, each
storing a value of type elt, can appear (like the 9—17 branch in
Figure 12.7).
• Finally, regular nodes of the tree store a value of type elt and have
two subtrees, a left and right subtree. (See for example, the node
with value 3 in Figure 12.7.) The node also stores its “balance” as
described below.
We will call a balanced tree odd or even. A tree is odd if its left child has
one more node than its right child. A tree is even if its children are of
equal size. The invariant says then that all subtrees must be either odd
or even.
Functions over the type will often need to respect combinations of
the ordering invariant and the balance invariant:
The add and take functions must return trees that respect the strong
invariant, and should assume they will only be passed trees that also
obey the strong invariant. That is, they preserve the strong invariant.
We have provided stubs for helper functions that operate on trees that
are required to preserve only the weak invariant. Hint: Your nodes
should track whether they are odd or even. This will help you keep
your tree balanced at all times.
Notice that we have encoded the difference between odd and even
nodes in the tree type that we’ve provided for BinaryHeap. You
should probably first write a size function for your tree type. This
will help you check your representation invariant. You should not be
calling size in the implementation of take; rather, you should be us-
ing size to test take. We have provided you with the implementation
of add and a partial implementation of take. Below are some guide-
lines when implementing take and its helper functions, as well as in
understanding add.
add The add function inserts a node into a spot that will either turn
the main tree from odd to even or from even to odd. We implement
this function for you, but you should understand how it works.
take The take function removes the root of the tree (the minimum
element) and replaces it by a leaf of the tree that, when removed, turns
the tree from odd to even or from even to odd.
After removing and replacing the root node your tree will respect
the weak invariant. You must “fix” the tree to respect the strong invari-
ant, as depicted in Figure 12.7.
Some questions to consider:
• How might you test the helper functions used in implementing your
binary heap?
A B S T R A C T D ATA T Y P E S A N D M O D U L A R P R O G R A M M I N G 213
3 25 7 7
9 7 9 7 9 25 9 15
17 25 15 17 25 15 17 15 17 25
type c
val sort : c list -> c list
Exercise 135
For brevity, we left off unary operators. Extend the grammar to add unary operators
(negation, say).
With this grammar, we can express the abstract syntax of the con-
crete expression
let x = 3 in
let y = 5 in
x * y
218 PROGRAMMING WELL
as the tree
⟨expr ⟩
3 y ⟨integer ⟩ ⟨expr ⟩
⟨var ⟩ * ⟨var ⟩
x y
What rules shall we use for evaluating the expressions of the lan-
guage? Recall that we write a judgement P ⇓ v to mean that the expres-
sion P evaluates to the value v. The VA L U E S , the results of evaluation,
are those expressions that evaluate to themselves. By convention, we’ll
use italic capitals like P , Q, etc. to stand for arbitrary expressions, and
v (possibly subscripted) to stand for expressions that are values. You
should think of P and v as expressions structured as per the abstract
syntax of the language – it is the abstract, structured expressions that
have well-defined meanings by the rules we’ll provide – though we
notate them using the concrete syntax of OCaml, since we need some
linear notation for specifying them.
Certain cases are especially simple. Numeric literal expressions like
3 or 5 are already as simplified as they can be. They evaluate to them-
selves; they are values. We could enumerate a plethora of judgements
that express this self-evaluation, like
1⇓1
2⇓2
3⇓3
4⇓4
5⇓5
⋯
but we’d need an awful lot of them. Instead, we’ll just use a schematic
rule for capturing permissible judgements:
n⇓n (R int )
SEMANTICS: THE SUBSTITUTION MODEL 219
Here, we use a schematic variable n to stand for any integer, and use
the notation n for the OCaml numeral expression that encodes the
number n.
Using this schematic rule notation we can provide general rules for
evaluating other arithmetic expressions. To evaluate an expression of
the form P + Q, where P and Q are two subexpressions, we first need
to know what values P and Q evaluate to; since they will be numeric
values, we can take them to be m and n, respectively. Then the value
that P + Q evaluates to will be m + n. We’ll write the rule as follows:
P + Q⇓
P ⇓m
∣ (R + )
Q ⇓n
⇓ m +n
In this rule notation, the first line is intended to indicate that we are
evaluating P + Q, the blank space to the right of the ⇓ indicating that
some further evaluation judgements are required. Those are the two
indented judgements provided to the right of the long vertical bar
between the two occurrences of ⇓. The final line provides the value
that the original expression evaluates to.
Thus, this rule can be glossed as “To evaluate an expression of
the form P + Q, first evaluate P to an integer value m and Q to an
integer value n. The value of the full expression is then the integer
literal representing the sum of m and n.”
Using these two rules, we can now show a particular evaluation, like
that of the expression 3 + 5:2 2
Wait, where did that 8 come from
exactly? Since 3 ≡ 3 and 5 ≡ 5, the rule
3 + 5⇓ R int gives the result as 3 + 5 ≡ 8 ≡ 8.
3⇓3
∣
5⇓5
⇓8
or the evaluation of 3 + 5 + 7:
3 + 5 + 7⇓
RRR
RRR 3 + 5 ⇓
RRR 3⇓3
RRR ∣
RRR 5⇓5
RRR
RRR ⇓8
RRR
RRR 7 ⇓ 7
R
⇓ 15
220 PROGRAMMING WELL
Exercise 136
Why is the proof for the value of 3 + 5 + 7 not structured as
3 + 5 + 7⇓
RRR 3 ⇓ 3
RRR
RRR 5 + 7 ⇓
RRR
RRR 5⇓5
RRR ∣
RRR 7⇓7
RRR
RRR ⇓ 12
⇓ 15 ?
P / Q⇓
P ⇓m
∣
Q ⇓n ,
⇓ ⌈m /n ⌉
which specifies that the result of the division is the integer resulting
from rounding up, rather than down.
Nonetheless, there is not too much work being done by these rules,
and if that were all there were to defining a semantics, there would be
little reason to go to the trouble. Things get more interesting, however,
SEMANTICS: THE SUBSTITUTION MODEL 221
Exercise 137
Write evaluation rules for the other binary operators and the unary operators you added
in Exercise 135.
let x = D in B ⇓
D ⇓ vD
∣ (R let )
B [x ↦ v D ] ⇓ v B
⇓ vB
(x * x)[x ↦ 5] = 5 * 5
let x = 5 in x * x ⇓ 25
222 PROGRAMMING WELL
let x = 5 in x * x ⇓
RRR
RRR 5 ⇓ 5
RRR 5
RRR * 5⇓
RRR 5⇓5
RRR ∣
RRR 5⇓5
RRR
RRR ⇓ 25
R
⇓ 25
Let’s put this first derivation together step by step so the steps are
clear. We want a derivation that demonstrates what let x = 5 in x
* x evaluates to. It will be of the form
let x = 5 in x * x ⇓
∣ ⋮
⇓⋯
This pattern matches rule R let , where x plays the role of the schematic
variable x, 5 plays the role of the schematic expression D, and x *
x plays the role of B . We will plug these into the two subderivations
required. First is the subderivation evaluating D (that is, 5):
let x = 5 in x * x ⇓
RRR 5 ⇓
RRR
RRR ∣ ⋮
RRR
RRR ⇓ ⋯
RRR
RRR ⋯
⇓⋯
This subderivation can be completed using the R int rule, which re-
quires no subderivations itself.
let x = 5 in x * x ⇓
RRR 5 ⇓
RRR
RRR ∣
RRR
RRR ⇓ 5
RRR
RRR ⋯
⇓⋯
Now
B [x ↦ v D ] = (x * x)[x ↦ 5]
= x[x ↦ 5] * x[x ↦ 5]
=5 * 5
let x = 5 in x * x ⇓
RRR
RRR 5 ⇓ 5
RRR 5
RRR * 5⇓
RRR ∣ ⋮
RRR
RRR ⇓⋯
R
⇓⋯
let x = 5 in x * x ⇓
RRR
RRR 5 ⇓ 5
RRR 5
RRR * 5⇓
RRR 5⇓m
RRR ∣
RRR 5⇓n
RRR
RRR ⇓ m ⋅n
R
⇓⋯
let x = 5 in x * x ⇓
RRR
RRR 5 ⇓ 5
RRR 5
RRR * 5⇓
RRR 5⇓5
RRR ∣
RRR 5⇓5
RRR
RRR ⇓ 25
R
⇓ 25
Exercise 138
Carry out derivations for the following expressions:
1. let x = 3 in let y = 5 in x * y
224 PROGRAMMING WELL
2. let x = 3 in let y = x in x * y
3. let x = 3 in let x = 5 in x * y
4. let x = 3 in let x = x in x * x
5. let x = 3 in let x = y in x * x
Are the values for these expressions according to the semantics consistent with how
OCaml evaluates them?
m [x ↦ Q ] = m
x [x ↦ Q ] = Q
y [x ↦ Q ] = y where x ≡
/y
(P + R )[ x ↦ Q ] = P [ x ↦ Q ] + R [ x ↦ Q ]
and similarly for other binary operators
(let y = D in B )[x ↦ Q ] = let y = D [x ↦ Q ] in B [x ↦ Q ]
Exercise 139
Verify using this definition for substitution the derivation above showing that
(x * x)[x ↦ 5] = 5 * 5.
You may have noticed in Exercise 138 that some care must be taken
when substituting. Consider the following case:
let x = 3 in let x = 5 in x
let x = 3 in let x = 5 in x
⇓
RRR
RRR 3 ⇓ 3
RRR let x = 5 in 3 ⇓
RRR
RRR 5⇓5
RRR ∣
RRR 3⇓3
RRR
RRR ⇓3
R
⇓3
(let x = 5 in x)[x ↦ 3] .
(let x = 5 in x)[x ↦ 3]
= let x = 5[x ↦ 3] in x[x ↦ 3]
= let x = 5 in x[x ↦ 3]
= let x = 5 in 3 .
Exercise 140
In the following expressions, draw a line connecting each bound variable to the binding
construct that binds it. Then circle all of the free occurrences of variables.
1. x
2. x + y
226 PROGRAMMING WELL
3. let x = 3 in x
4. let f = f 3 in x + y
5. (fun x -> x + x) x
of example, the definition says that the free variables in the expres-
sion fun y -> f (x + y) are just f and x, as shown in the following
derivation:
Exercise 141
Use the definition of F V to derive the set of free variables in the expressions below.
Circle all of the free occurrences of the variables.
1. let x = 3 in let y = x in f x y
2. let x = x in let y = x in f x y
3. let x = y in let y = x in f x y
Exercise 142
The definition of F V in Figure 13.3 is incomplete, in that it doesn’t specify the free
variables in a let rec expression. Add appropriate rules for this construct of the
language, being careful to note that in an expression like let rec x = fun y -> x in
x, the variable x is not free. (Compare with Exercise 141(4).)
Now that we have formalized the idea of free and bound variables,
it may be clearer what is going wrong in the previous substitution
example. The substitution rule for substituting into a let expression
shouldn’t apply when x and y are the same variable. In such a case, the
occurrences of x in D or B are not free occurrences, but are bound by
the let. We modify the definition of substitution accordingly:
SEMANTICS: THE SUBSTITUTION MODEL 227
m [x ↦ Q ] = m
x [x ↦ Q ] = Q
y [x ↦ Q ] = y where x ≡
/y
(P + R )[ x ↦ Q ] = P [ x ↦ Q ] + R [ x ↦ Q ] and similarly for other binary operators
(let y = D in B )[x ↦ Q ] = let y = D [x ↦ Q ] in B [x ↦ Q ] where x ≡
/y
(let x = D in B )[x ↦ Q ] = let x = D [x ↦ Q ] in B
Exercise 143
Use the definition of the substitution operation above to give the expressions (in con-
crete syntax) specified by the following substitutions:
1. (x + x)[x ↦ 3]
2. (x + x)[y ↦ 3]
3. (x * x)[x ↦ 3 + 4]
4. (let x = y in y + x)[y ↦ z]
5. (let x = y in y + x)[x ↦ z]
Exercise 144
Use the semantic rules developed so far (see Figure 13.5) to reduce the following expres-
sions to their values. Show the derivations.
1. let x = 3 * 4 in
x + x
2. let y = let x = 5
in x + 1
in y + 2
# type expr =
# | Int of int
# | Var of varspec
# | Binop of binop * expr * expr
# | Let of varspec * expr * expr ;;
type expr =
Int of int
| Var of varspec
| Binop of binop * expr * expr
| Let of varspec * expr * expr
let x = 3 in
let y = 5 in
x / y
Exercise 145
Augment the type definitions to allow for other binary operations (subtraction and
multiplication, say) and for unary operations (negation).
Exercise 146
Write a function subst : expr -> varspec -> expr -> expr that performs substi-
tution, that is, subst p x q returns the expression that is the result of substituting q for
the variable x in the expression p. For example,
SEMANTICS: THE SUBSTITUTION MODEL 229
The computation for each of the cases mimics the computations in the
evaluation rules exactly. Integers, for instance, are self-evaluating.
let rec eval (exp : expr) : expr =
match exp with
| Int n -> Int n
| Var x -> ...
| Binop (Plus, e1, e2) -> ...
| Binop (Divide, e1, e2) -> ...
| Let (var, def, body) -> ...
The second pattern concerns what should be done for evaluating free
variables in expressions. (Presumably, any bound variables were sub-
stituted away by virtue of the final pattern-match.) We have provided
no evaluation rule for free variables, and for good reason. Expressions
with free variables, called O P E N E X P R E S S I O N S don’t have a value in
and of themselves. Consequently, we can simply report an error upon
evaluation of a free variable. We introduce an exception for this pur-
pose.
let rec eval (exp : expr) : expr =
match exp with
| Int n -> Int n
| Var x -> raise (UnboundVariable x)
| Binop (Plus, e1, e2) -> ...
| Binop (Divide, e1, e2) -> ...
| Let (var, def, body) -> ...
230 PROGRAMMING WELL
12 | Int (m / n)
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(Var _|Binop (_, _, _)|Let (_, _, _))
Lines 10-12, characters 0-11:
10 | let Int m = eval e1 in
11 | let Int n = eval e2 in
12 | Int (m / n)
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
(Var _|Binop (_, _, _)|Let (_, _, _))
val eval : expr -> expr = <fun>
Exercise 147
Augment the abstract syntax of the language to introduce boolean literals true and
false. Add substitution semantics rules for the new constructs. Adjust the definitions of
subst and eval to handle these new literals.
Exercise 148
Augment the abstract syntax of the language to add conditional expressions (if ⟨⟩
then ⟨⟩ else ⟨⟩ ). Add substitution semantics rules for the new construct. Adjust the
definitions of subst and eval to handle conditionals.
P Q⇓
RRR P ⇓ fun x -> B
RRR
RRR Q ⇓ v (R app )
RRR Q
RRR B [x ↦ v ] ⇓ v
R Q B
⇓ vB
Exercise 149
Give glosses for these two rules R fun and R app , as was done for the previous rules R + and
R let .
Let’s try an example:
(fun x -> x + x) (3 * 4)
(fun x -> x + x) (3 * 4)
⇓
RRR (fun x -> x + x) ⇓ (fun x -> x + x)
RRR
RRR 3
RRR * 4⇓
RRR 3⇓3
RRR ∣
RRR 4 ⇓4
RRR
RRR ⇓ 12
RRR
RRR 12 + 12 ⇓
RRR
RRR 12 ⇓ 12
RRR ∣
RRR 12 ⇓ 12
RRR
RRR ⇓ 24
⇓ 24
⇓
RRR fun x -> 2
RRR * x ⇓ fun x -> 2 * x
RRR (fun x -> 2 * x) ((fun x -> 2 * x) 3)
RRR
RRR ⇓
RRR
RRR RRR fun x -> 2
RRR RRR * x ⇓ fun x -> 2 * x
RRR RRR (fun x -> 2 * x) 3
RRR RRR
RRR RRR ⇓
RRR RRR
RRR R RRR fun x -> 2
RRR RRR * x ⇓ fun x -> 2 * x
RRR RRR R
RRR RRR R
RRR 3 ⇓ 3
RRR RRR RRR
RRR RRR RRR 2 * 3 ⇓
RRR RRR RRR
RRR RRR RRR 2⇓2
RRR RRR RRR ∣
RRR RRR RRR 3 ⇓3
RRR RRR RRR
RRR RRR R ⇓6
RRR RRR
RRR RRR ⇓6
RRR RRR 2
RRR R * 6 ⇓ 12
RRR ⇓ 12
R
⇓ 12
Exercise 150
1. (fun x -> x + 2) 3
following derivation:
⇓
RRR fun z -> y ⇓ fun z -> y
RRR
RRR (fun y -> (fun z -> y) 3) 1
RRR
RRR ⇓
RRR
RRR RRR (fun y -> (fun z > y) 3) ⇓ (fun y -> (fun z -> y) 3)
RRR RRR
RRR RRR 1 ⇓ 1
RRR RRR
RRR RRR (fun z -> 1) 3 ⇓
RRR RRR
RRR RRR fun z -> 1 ⇓ fun z -> 1
RRR RRR ∣
RRR RRR 1⇓1
RRR RRR
RRR RRR ⇓1
RRR
RRR ⇓1
⇓1
The problem happens in the highlighted expression. We’re sneaking
a y inside the scope of the variable y bound by the fun. That’s not
kosher. We need to change the definition of substitution to make sure
that such VA R I A B L E C A P T U R E doesn’t occur. The following rules for
substituting inside a function work by replacing the bound variable y
with a new freshly minted variable, say z, that doesn’t occur elsewhere,
renaming all occurrences of y accordingly.
where x ≡
/ y and y ∈/ F V (Q )
where x ≡
/ y and y ∈ F V (Q ) and z is a fresh variable
Exercise 151
Carry out the derivation for
let f = fun z -> y in (fun y -> f 3) 1
as above but with this updated definition of substitution. What happens at the step
highlighted above?
Exercise 152
What should the corresponding rule or rules defining substitution on let ⋯ in ⋯
expressions be? That is, how should the following rule be completed? You’ll want to think
about how this construct reduces to function application in determining your answer.
(let y = Q in R )[x ↦ P ] = ⋯
Try to work out your answer before checking it with the full definition of substitution in
Figure 13.4.
236 PROGRAMMING WELL
Exercise 153
Use the definition of the substitution operation above to determine the results of the
following substitutions:
3. (let x = y * y in x + x)[x ↦ 3]
4. (let x = y * y in x + x)[y ↦ 3]
Exercise 154
Write a function free_vars : expr -> varspec Set.t that returns a set of varspecs
corresponding to the free variables in the expression as per Figure 13.3. (Recall the
discussion of the OCaml library module Set in Section 12.8.)
Exercise 155
Revise the definition of subst to eliminate the problem of variable capture by imple-
menting the set of rules given in Figure 13.4.
F V (m ) = ∅ (integers) (13.1)
F V (x ) = {x } (variables) (13.2)
F V (P + Q ) = F V (P ) ∪ F V (Q ) (and similarly for other binary operators) (13.3)
F V (P Q ) = F V (P ) ∪ F V (Q ) (applications) (13.4)
F V (fun x -> P ) = F V (P ) − {x } (functions) (13.5)
F V (let x = P in Q ) = (F V (Q ) − {x }) ∪ F V (P ) (binding) (13.6)
You may observe that the rule for evaluating let ⟨⟩ in ⟨⟩ expressions
doesn’t allow for recursion. For instance, the Fibonacci example pro-
SEMANTICS: THE SUBSTITUTION MODEL 237
m [x ↦ P ] = m (13.7)
x [x ↦ P ] = P (13.8)
y [x ↦ P ] = y where x ≡
/y (13.9)
(Q + R )[x ↦ P ] = Q [x ↦ P ] + R [x ↦ P ] (13.10)
and similarly for other binary operators
Q R [x ↦ P ] = Q [x ↦ P ] R [x ↦ P ] (13.11)
(fun x -> Q )[x ↦ P ] = fun x -> Q (13.12)
(fun y -> Q )[x ↦ P ] = fun y -> Q [x ↦ P ] (13.13)
where x ≡
/ y and y ∈/ F V (P )
(fun y -> Q )[x ↦ P ] = fun z -> Q [ y ↦ z ][x ↦ P ] (13.14)
where x ≡
/ y and y ∈ F V (P ) and z is a fresh variable
(let x = Q in R )[x ↦ P ] = let x = Q [x ↦ P ] in R (13.15)
(let y = Q in R )[x ↦ P ] = let y = Q [x ↦ P ] in R [x ↦ P ] (13.16)
where x ≡
/ y and y ∈/ F V (P )
(let y = Q in R )[x ↦ P ] = let z = Q [x ↦ P ] in R [ y ↦ z ][x ↦ P ] (13.17)
where x ≡
/ y and y ∈ F V (P ) and z is a fresh variable
ceeds as follows:
⇓
RRR fun n -> if n = 0 then 1 else n
RRR * f (n - 1) ⇓ fun n -> if n = 0 then 1 else n * f (n - 1)
RRR (fun n -> if n = 0 then 1 else n
RRR * f (n - 1)) 2
RRR ⇓
RRR
RRR RRR fun n -> if n = 0 then 1 else n
RRR RRR * f (n - 1) ⇓ fun n -> if n = 0 then 1 else n * f (n - 1)
RRR RRR 2 ⇓ 2
RRR RRR
RRR RRR if 2 = 0 then 1 else 2
RRR R * f (2 - 1) ⇓ ???
RRR ⇓ ???
⇓ ???
let rec x = D in B ⇓
D ⇓ vD
∣
B [x ↦ v D [x ↦ let rec x = v D in x ]] ⇓ v B
⇓ vB
(R letrec )
if 2 = 0 then 1
else 2 * (let rec f = fun n -> if n = 0 then 1
else n * f (n-1) in f) (2-1))
Exercise 156
Thanklessly continue this derivation until it converges on the final result for the factorial
of 2, viz., 2. Then thank your lucky stars that we have computers to do this kind of rote
repetitive task for us.
We’ll provide an alternative approach to semantics of recursion
when we introduce environment semantics in Chapter 19.
let f x = x + 1
this expression can be taken as syntactic sugar for (that is, a variant
concrete syntax for the abstract syntax of) the expression
P + Q⇓
P ⇓m
∣ (R + )
Q ⇓n
⇓ m +n
P / Q⇓
P ⇓m
∣ (R / )
Q ⇓n
⇓ ⌊m /n ⌋
P Q⇓
RRR P ⇓ fun x -> B
RRR
RRR Q ⇓ v (R app )
RRR Q
RRR B [x ↦ v ] ⇓ v
R Q B
⇓ vB
let x = D in B ⇓
D ⇓ vD
∣ (R let )
B [x ↦ v D ] ⇓ v B
⇓ vB
let rec x = D in B ⇓
D ⇓ vD
∣
B [x ↦ v D [x ↦ let rec x = v D in x ]] ⇓ v B
⇓ vB
(R letrec )
14
Efficiency, complexity, and recurrences
We say that some agent is efficient if it makes the best use of a scarce
resource to generate a desired output. Furnaces turn the scarce re-
source of fuel into heating, so an efficient furnace is one that generates
the most heat using the least fuel. Similarly, an efficient shooter in
basketball generates the most points using the fewest field goal at-
tempts. Standard measurements of efficiency reflect these notions.
Furnaces are rated for Annual Fuel Utilization Efficiency, NBA players
for Effective Field Goal Percentage.
Computer programs use scarce resources to generate desired out-
puts as well. Most prominently, the resources expended are time and
“space” (the amount of memory required during the computation),
though power is increasingly becoming a resource of interest.
Up to this point, we haven’t worried about the efficiency of the pro-
grams we’ve written. And for good reason. Donald Knuth, Professor
Emeritus of the Art of Computer Programming at Stanford Univer-
sity and Turing-Award–winning algorithmist, warns of P R E M AT U R E
O P T I M I Z AT I O N :
• Which input? How many elements are in the list? What order are
they in? Are there a lot of duplicate items, or very few?
• How computed? Which computer are you using, and which soft-
ware environment? How long does it take to execute the primitive
computations out of which the function is built?
All of these issues affect the running time of a particular sorting func-
tion. To make any progress on comparing the efficiency of functions in
the face of such intricacy, it is clear that we will need to come up with a
more abstract way of characterizing the efficiency of computations.
We address these two issues separately. To handle the question of
“which input”, we might characterize the efficiency of the sorting pro-
gram not as a number (a particular running time), but as a function
from inputs to numbers. However, this doesn’t seem an appealing
option; we want to be able to draw some general conclusions for com-
paring sorting programs, not have to reassess for each possible input.
Nonetheless, the idea of characterizing efficiency in terms of some
function is a useful one. Broadly speaking, algorithms take longer on
bigger problems, so we might use a function that provides the time re-
quired as a function of the size of the input. In the case of sorting lists,
we might take the size of the input to be the number of elements in the
list to be sorted. Unfortunately, for any given input size, the program
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 243
# match xs with
# | [] -> []
# | hd :: tl -> insert lt (sort lt tl) hd
# end ;;
module InsertSort : SORT
Exercise 157
Provide implementations of the functions split and merge, and package them together
with the sort function just provided in a module MergeSort satisfying the SORT module
type. You should then have a module that allows for the following interactions:
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 245
T i s (n ) = a ⋅ n 2 + b
whereas for mergesort, the time required to sort the list grows as the
function
Tms (n ) = c ⋅ n log n + d
1. How to figure out the growth function for a given algorithm, and
In the remainder of this chapter, we will address the first of these with
a technique of recurrence equations, and the second with the idea of
asymptotic complexity and “big-O” notation.
that grows quadratically (as the square of the size) like the former will
eventually outstrip a function that grows like the latter. Figure 14.5
shows this graphically. The gray lines all grow as c ⋅ n log n for increas-
ing values of c. But regardless of c, the red line, displaying quadratic
growth, eventually outpaces all of the gray lines. In a sense, then, we’d
eventually like to use the n log n algorithm regardless of the constants.
It is this A S Y M P T OT I C (that is, long term or eventual) sense that we’d
like to be able to characterize.
To address the question of how fast a function grows asymptotically,
independent of the annoying constants, we introduce a generic way of
expressing the growth rate of a function – B I G -O N OTAT I O N .
We’ll assume that problem sizes are non-negative integers and that
times are non-negative as well. Given a function f from non-negative
integers to non-negative numbers, O ( f ) is the set of functions that
grow no faster than f , in the following precise sense:3 We define O ( f )
to be the set of all functions g such that for all “large enough” n (that is, Figure 14.5: A graph of functions with
different growth rates. The highlighted
n larger than some value n 0 ), g (n ) ≤ c ⋅ f (n ). line grows as n 2 . The three gray lines
The roles of the two constants n 0 and c are exactly to move beyond grow as c ⋅ n log n, where c is, from
bottom to top, 1, 2, and 4.
the details of constants like the a, b, c, and d in the sorting algorithm 3
Since it takes a function as its argu-
growth functions. In deciding whether a function grows no faster than ment and returns sets of functions as
f , we don’t want to be misled by a few input values here and there its output, O is itself a higher-order
function!
where g (n ) may happen to be larger than f (n ), so we allow exempting
values smaller than some fixed value n 0 . The point is that as the inputs
grow in size, eventually we’ll get past the few input sizes n where g (n )
is larger than f (n ). Similarly, if the value of g (n ) is always, say, twice
the value of f (n ), the two aren’t growing at qualitatively different rates.
Perhaps that factor of 2 is based on just the kinds of idiosyncrasies that
can change as computers change. We want to ignore such constant
multiplicative factors. For that reason, we don’t require that g (n ) be
less than f (n ); instead we require that g (n ) be less than some constant
multiple c of f (n ).
As an example of big-O notation, consider two simple polynomial
functions. It will be convenient to use Church’s elegant lambda nota-
tion (see Section A.1.4) to specify these functions directly: λn.10n 2 + 3
and λn.n 2 .
Is the function λn.10n 2 + 3 an element of the set O (λn.n 2 )? To
demonstrate that it is, we need to find constants c and n 0 such that
for all n > n 0 , 10n 2 + 3 ≤ c ⋅ n 2 . It turns out that the values n 0 = 0 and
c = 13 do the trick, that is, for all n > 0, 10n 2 + 3 ≤ 13n 2 . We can prove
this as follows: Since n ≥ 1, it follows that n 2 ≥ 1 and thus 3 ≤ 3n 2 . Thus
10n 2 + 3 ≤ 10n 2 + 3n 2 = 13n 2 . We conclude, then, that
λn.10n 2 + 3 ∈ O (λn.n 2 ) .
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 249
λn.n 2 ∈ O (λn.10n 2 + 3) .
We can just take n 0 again to be 0 and c to be 1, since n 2 < 10n 2 + 3 for all
n.
10n 2 + 3 ∈ O (n 2 ) ,
f ∈ O( f )
k ⋅ g ∈ O( f ) .
f + g ∈ O (n k )
The upshot of all this is that in determining the big-O growth rate
of a polynomial function, we can always just drop lower degree terms
and multiplicative constants. In thinking about the growth rate of a
complicated function like 4n 3 + 142n + 3, we can simply ignore all but
the largest degree term (4n 3 ) and even the multiplicative constant 4,
and conclude that
4n 3 + 142n + 3 ∈ O (n 3 )
Exercise 158
Which of these claims about the growth rates of various functions hold?
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 251
1. 3n + 5 ∈ O (n )
2. n ∈ O (3n + 5)
3. n + n 2 ∈ O (n )
4. n 3 + n 2 ∈ O (n 3 + 2n )
5. n 2 ∈ O (n 3 )
6. n 3 ∈ O (n 2 )
7. 32n 3 ∈ O (n 2 + n + k )
f ′ + g ′ ∈ O( f + g )
f ′ ⋅ g ′ ∈ O( f ⋅ g )
n 2 ∈ O (n 3 ) ,
n 3 ∈/ O (n 2 ) .
n3 ≫ n2 ,
nk ≫ nc when k > c
n ≫ log n
252 PROGRAMMING WELL
2n ≫ n k
3 n ≫ 2n
An appropriate measure for the size of the input to the function is the
sizes of the two lists it is to append. Let’s use Tappend (n, m ) for the time
required to run the append function on lists with n and m elements
respectively. What do we know about this Tappend ?
When the first argument, xs, is the empty list (so n = 0), the function
performs just a few simple actions, pattern-matching the input against
the empty list pattern, and then returning ys. If we say that the time for
the pattern match is some constant c match and the time for the return
is some constant c returnys , then we have that
Since the sum of the two constants is itself a constant, we can simplify
by treating the whole as a new constant c:
Tappend (0, m ) = c
Tappend (0, m ) = c
Tappend (n + 1, m ) = k + Tappend (n, m )
254 PROGRAMMING WELL
Continuing in this vein, we can continue to unfold until the first argu-
ment to Tappend becomes 0:
How many unfoldings are required until the first argument reaches 0?
We’ll have had to unfold n times. There will therefore be n instances of
k being summed in the unfolded equation. Completing the derivation,
then, using the first recurrence equation,
Tappend (n, m ) = k ⋅ n + c
The closed form solution for append from the previous section be-
comes useful here. And again, notice our free introduction of new
constants to simplify things. We take the sum of c match and c cons to be
r , then for r + c we introduce s. Summarizing, the reverse implemen-
tation above yields the recurrence equations
Trev (0) = q
Trev (n + 1) = k ⋅ n + s + Trev (n )
let compare_lengths xs ys =
compare (List.length xs) (List.length ys) ;;
For instance,
# compare_lengths [1] [2; 3; 4] ;;
- : int = -1
# compare_lengths [1; 2; 3] [4] ;;
- : int = 1
# compare_lengths [1; 2] [3; 4] ;;
- : int = 0
However, this implementation of compare_lengths does a little extra work than it needs
to. Its complexity is O (n ) where n is the length of the longer of the two lists.
Why does compare_lengths have this big-O complexity? In particular, why does
the length of the shorter list not play a part in the complexity? We’re looking for a brief
informal argument here, not a full derivation of its complexity.
Provide an alternative implementation of compare_lengths whose complexity is
O (n ) where n is the length of the shorter of the two lists, not the longer. Figure 14.6: A graphical proof that
n n ⋅ (n + 1 )
∑i = .
14.5.3 Complexity of reversing a list with accumulator i =1 2
Trevapp (0, m ) = c
Trevapp (n + 1, m ) = k + Trevapp (n, m + 1)
Trevapp (n, m ) = k ⋅ n + c
∈ O (n )
so that
Tinsert (0) = c
Inserting into a nonempty list (of size n + 1) is more subtle. The time
required depends on whether the element should come at the start
of the list (the else clause of the conditional) or not (the then clause).
In the former case, the cons operation takes constant time, say k 2 ; in
the latter case, it involves a recursive call to insert (Tinsert (n )) plus
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 259
Tinsert (n ) = k + Tinsert (n − 1)
= k + k + Tinsert (n − 2)
=⋯
= k ⋅ n + Tinsert (0)
= k ⋅n +c
∈ O (n )
Tisort (0) = c
Tisort (n + 1) = k + Tisort (n ) + Tinsert (n )
Tisort (n ) = k + Tisort (n − 1) + O (n − 1)
= k + k + Tisort (n − 2) + O (n − 1) + O (n − 2)
= k ⋅ n + Tisort (0) + O (n − 1) + O (n − 2) + ⋯ + O (0)
n
= k ⋅ n + c + ∑ O (i )
i =1
∈ O (n )
2
takes two list arguments; their sizes will be two of the arguments of the
complexity function Tmerge . Each recursive call of merge reduces the
total number of items in the two lists. We will for that reason use the
sum of the sizes of the two lists as the argument to Tmerge .
If the total number of elements in the two lists is 1, then one of the
two lists must be empty, and we have
Tmerge (1) = c
In the worst case, neither element will become empty until the to-
tal number of elements in the lists is 2. Thus, for n ≥ 2, we have the
“normal” case, when the lists are nonempty, which involves (in ad-
dition to some constant overhead) a recursive call to merge with one
fewer element in the lists. In the worst case, both elements will still be
nonempty.
Tmerge (n + 1) = k + Tmerge (n )
Tmerge (n ) = k + Tmerge (n − 1)
= k + k + Tmerge (n − 2)
=⋯
= k ⋅ n + Tmerge (1)
= k ⋅n +c
∈ O (n )
| []
| [_] -> lst, []
| first :: second :: rest ->
let first', second' = split rest in
first :: first', second :: second' ;;
Exercise 161
Show that split has time complexity linear in the size of its first list argument.
T (1 ) = c
and a recursive case that involves two recursive calls on some prob-
lems each of half the size. At first, we’ll assume that the time to break
apart and put together the two parts takes constant time k.
T (n ) = k + 2 ⋅ T (n /2)
T (n ) = k + 2 ⋅ T (n /2)
= k + k + 4 ⋅ T (n /4 )
= k + k + k + 8 ⋅ T (n /8)
=⋯
T (n ) = k ⋅ n + 2 ⋅ T (n /2)
n 2 ≫ n log n
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 263
Recall the Luhn check algorithm from Section 9.6, and its various
component functions: evens, odds, doublemod9, sum.
Problem 162
What is an appropriate recurrence equation for defining the time complexity of the odds
function from Problem 69 in terms of the length of its list argument?
Problem 163
What is the time complexity of the odds function from Problem 69 (in big-O notation)?
Problem 164
If the function f (n ) is the time complexity of odds on a list of n elements, which of the
following is true?
• f ∈ O (1 )
• f ∈ O (log n )
• f ∈ O (log n /c ) for all c > 0
• f ∈ O (c ⋅ log n ) for all c > 0
• f ∈ O (n )
• f ∈ O (n /c ) for all c > 0
• f ∈ O (c ⋅ n ) for all c > 0
• f ∈ O (n 2 )
264 PROGRAMMING WELL
Problem 165
What is the time complexity of the luhn function implemented in Problem 73 in terms of
the length n of its list argument? Use big-O notation. Explain why your implementation
has that complexity.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
3 2 7
(a) (b) (c)
(a) (b)
1 6 4 1 4 1 6 4 1 6 4
5 2 8 5 6 8 5 8 5 8
3 7 3 2 7 3 2 7 3 2 7
(c) (d) (e) (f)
5 8
3 2 7
down left
up right
1 4 1 6 4 1 6 4 1 6 4
5 6 8 5 2 8 5 8 5 8
3 2 7 3 7 3 2 7 3 2 7
state from the pending collection and test it to see if it is a goal state. If
so, the puzzle has been solved. But if not, this state’s N E I G H B O R states
– states that are reachable in one move from the current state – are
added to the pending collection (or at least those that have not been
visited before) and the search continues.
To avoid adding states that have already been visited before, you’ll
need to keep track of a set of states that have already been visited,
which we’ll call the visited set, so you don’t revisit one that has already
been visited. For instance, in the 8 puzzle, after a down move, you don’t
want to then perform an up move, which would just take you back to
where you started. (The standard OCaml Set library will be useful here
to keep track of the set of visited states.)
Of course, much of the effectiveness of this process depends on the
order in which states are taken from the collection of pending states as
the search proceeds. If the states taken from the collection are those
most recently added to the collection (last-in, first-out, that is, as a
stack), the tree is being explored in a D E P T H - F I R S T manner. If the
states taken from the collection are those least recently added (first-in,
first-out, as a queue), the exploration is B R E A D T H - F I R S T . Other orders
are possible, for instance, the states might be taken from the collection
in order of how closely they match the goal state (using some metric
of closeness). This regime corresponds to B E S T- F I R S T or G R E E DY
S E A R C H.
• collections.ml
• puzzledescription.ml
• puzzlesolve.ml
The search proceeds by taking a state from the pending collection (call
it the current state). If the current state has already been visited (that
is, it’s in the visited set), we can move on to the next pending state. But
E F F I C I E N C Y, C O M P L E X I T Y, A N D R E C U R R E N C E S 269
if it has never been visited, we add it to the visited set and examine it
further. If it is a goal state, the search is over and appropriate infor-
mation can be returned from the search. If not, the neighbors of the
current state are generated and added to the pending collection, and
the search continues.
You will implement a functor MakePuzzleSolver that maps a col-
lection implementation (a functor that generates modules satisfy-
ing the signature COLLECTION) and a puzzle description (a module
satisfying the PUZZLEDESCRIPTION signature) to a module imple-
menting the PUZZLESOLVER signature. The solve function in the
generated PUZZLESOLVER module solves the puzzle described in the
PUZZLEDESCRIPTION using a search that uses the provided collection
regime.
The type signature of MakePuzzleSolver is
the first two problems in the problem set, you should be able to build
tests.byte and see the puzzle solvers in action.
Running the tests uses OCaml’s graphics to display the puzzles and
even animates the search process. After each display, press any key to
move on to the next display. Figure 14.11 shows the display of the maze
solver as it animates the solution of a maze.
This code should provide some inspiration for your own systematic
testing of the solver on these kinds of puzzles. You can even implement
your own puzzles if you’re of a mind too. (Extra karma!)
You can try out both depth-first and breadth-first search for solving
the puzzles by using different collection functors in the solver. With
your two-stack implementation of queues, you can experiment with Figure 14.11: A frame from an ani-
mation of the maze solver. The initial
the relative efficiency of the two queue implementations. position of the maze is in the upper left,
Problem 168 and the goal in the lower right, marked
In order to assess the benefit of the additional implementation of collections, you should with a grey circle on purple background.
design, develop, test, and deploy a performance testing system for timing the perfor- The current position is marked with the
mance of the solver using different search regimes and implementations. Unlike in pre- purple circle slightly southeast of the
vious problem sets, we provide no skeleton code to carry out this part of the problem set center. The maze walls are brick red,
(though tests.ml may be useful to look at). You should add a new file experiments.ml and the visited positions are marked
that carries out your experiments. The design of this code is completely up to you. This with small squares.
part of the problem set allows you the freedom to experiment with ab initio design. The
deliverable for this part of the problem set, in addition to any code that you write, is a
report on the relative performance that you find.
You’ll want to write some OCaml code to time the solving process on appropriate
examples with several collection implementations. There are some timing functions that
may be useful in the Absbook module (absbook.ml), which we’ve used in tests.ml. You
may also find the time and gettimeofday functions in the Sys and Unix modules useful
for this purpose.
We recommend that to the extent possible any code that you write for performing
your testing not change the structure of code we provide, so that our unit tests will still
work. Ideally, all the code for your experimentation should be in new files, not the ones
we provided.
You should generate your writeup with the rest of your code as a raw text, Markdown,
or LATEX file named writeup.txt, writeup.md, or writeup.tex. (We recommend these
markup-based non-wysiwyg formats for reasons described here. For Markdown in
particular, there are many tools for writing and rendering Markdown files. Some of our
favorites are: ByWord, Marked, MultiMarkdown Composer, Sublime Text, and the Swiss
army knife of file format conversion tools, pandoc.) If you use Markdown or LATEX, you
should include the rendered version as a PDF file writeup.pdf in your submission as
well.
# fib 20 ;;
- : int = 10946
a program does, not a value that a program has. Without that one side
effect, the fib computation would be useless. We’d gain no informa-
tion from it.
So we need at least a little impurity in any programming system.
But there are some algorithms that actually require impurity – side
effects that change state. For instance, we’ve seen implementation of
a dictionary data type in Chapter 12. That implementation allowed
for linear insertion and linear lookup. More efficient implementations
allow for constant time insertion and linear lookup (or vice versa) or
for logarithmic insertion and lookup. But by taking advantage of side
effects that change state, we can implement mutable dictionaries,
which achieve constant time insertion and constant time lookup, for
instance, with hash tables. (In fact, we do so in Section 15.6.)
In this chapter and the next, we introduce I M P E R AT I V E P R O G R A M -
M I N G , a programming paradigm based on side effects and state
change. We start with mutable data structures, moving on to imper-
ative control structures in the next chapter.
In the pure part of OCaml, we don’t change the state of the compu-
tation, as encoded in the computer’s memory. In languages that have
mutable state, variables name blocks of memory whose contents can
change. Assigning a new value to such a variable mutates the memory,
changing its state by replacing the original value with the new one.
OCaml variables, by contrast, aren’t mutable. They name values, and
once having named a value, the value named doesn’t change.
You might think that OCaml does allow changing the value of a
variable. What about, for instance, a global renaming of a variable?
# let x = 42 ;;
val x : int = 42
# x ;; (* x is 42 *)
- : int = 42
# let x = 21 ;;
val x : int = 21
# x ;; (* ...but now it's 21 *)
- : int = 21
The definition of the function f makes use of the first variable x, simply
by returning its value when called. Even if we add a new x naming a
different value, the application f () still returns 42, the value that the
first variable x names, thereby showing that the first x is still available.
The let naming constructs of OCaml thus don’t provide for mutable
state. If we want to make use of mutable state, for instance for the pur-
pose of building mutable data structures, we’ll need new constructs.
OCaml provides references for this purpose.
15.1 References
# !r ;;
- : int = 42
# r := 21 ;;
- : unit = ()
# !r ;;
- : int = 21
Here, we’ve dereferenced the same variable r twice (in the two high-
lighted expressions), getting two different values – first 42, then 21.
This is quite different from the example with two x variables. Here,
there is only one variable r, and yet a single expression !r involving r
whose value has changed!2 2
But like all variables, r has not itself
changed its value. It still points to the
This example puts in sharp relief the difference between the pure
same block of memory.
language and the impure. In the pure language, an expression in a
given lexical context (that is, the set of variable names that are avail-
able) always evaluates to the same value. But in this example, two
instances of the expression !r evaluate to two different values, even
though the same r is used in both instances of the expression. The
assignment has the side effect of changing what value is stored in the
block that r references, so that reevaluating !r to retrieve the stored
value finds a different integer.
The expression causing the side effect here was easy to spot. But
in general, these side effects could happen as the result of a series of
function calls quite obscure from the code that manifests the side
effect. This property of side effects can make it difficult to reason about
what value an expression has.
In particular, the substitution semantics of Chapter 13 has Leibniz’s
law as a consequence. Substitution of equals for equals doesn’t change
the value of an expression. But here, we have a clear counterexample.
The first evaluation implies that !r and 42 are equal. Yet if we substi-
tute 42 for !r in the third expression, we get 42 instead of 21. Once we
add mutable state to the language, we need to extend the semantics
from one based purely on substitution. We do so in Chapter 19, where
we introduce environment semantics.
ence to update, of type ’a ref, and the new ’a value to store there.
But what should the assignment operator return? Assignment is per-
formed entirely for its side effect – the update in the state of memory
– rather than for its return value. Given that there is no information
in the return value, it makes sense to use a type that conveys no in-
formation. This is a natural use for the unit type (Section 4.3). Since
unit has only one value (namely, the value ()), that value conveys no
information. The hallmark of a function that is used only for its side
effects (which we might call a P R O C E D U R E ) is the unit return type.
The typing for assignment is appropriately then (:=) : ’a ref ->
’a -> unit.
These typings can be verified in OCaml itself:
# (!) ;;
- : 'a ref -> 'a = <fun>
# (ref) ;;
- : 'a -> 'a ref = <fun>
# (:=) ;;
- : 'a ref -> 'a -> unit = <fun>
generates a new named box and its referent (Figure 15.1(b)), which
happens to store the same value. But we can tell that the referents are
distinct, since assigning to r changes !r but not !s (Figure 15.1(c)).
# r := 21 ;;
- : unit = ()
# !r, !s ;;
- : int * int = (21, 42)
Figure 15.1: Box and arrow diagrams
To have s refer to the value that r does, we need to assign to it as well for the state of memory as various
(Figure 15.1(d)). references are created and updated.
# s := !r ;;
- : unit = ()
# let s = r ;;
val s : int ref = {contents = 21}
Now s and r have the same value (that is, refer to the same block of
memory). We say that s is an A L I A S of r. (The old s is shadowed by the
new one, as depicted by showing it in gray. Since we no longer have
access to it and whatever it references, the gray blocks of memory are
garbage. See the discussion in Section 15.1.3.)
Changing the value stored in a block of memory changes the value
of all its aliases as well. Here, updating the block referred to by r (Fig-
ure 15.1(f)) changes the value for s:
# r := 7 ;;
- : unit = ()
# !r, !s ;;
- : int * int = (7, 7)
You may have seen this kind of thing before. In programming lan-
guages like c, references to blocks of memory are manipulated through
POINTERS to memory, which are explicitly created (with malloc) and
freed (with free), dereferenced (with *), and updated (with =). Some
correspondences between OCaml and c syntax for these operations are
given in Table 15.1.
Notable differences between the OCaml and c approaches are:
Operation OCaml c
that the block must at all times store an int and the operators
maintain this invariant. Table 15.1: Approximate equivalencies
between OCaml references and c
• In c, nothing conspires to make sure that the size of the block al- pointers.
4. let a = 2 in
let f = (fun b -> a * b) in
let a = 3 in
f (f a) ;;
Records (Section 7.4) are compound data structures with named fields,
each of which stores a value of a particular type. As introduced, each
field of a record, and hence records themselves, are immutable. How-
ever, when a record type is defined with the type construct, and the
individual fields are specified and typed, its individual fields can also
be marked as allowing mutability by adding the keyword mutable.
For instance, we can define a person record type with immutable
name fields but a mutable address field.
15.2.2 Arrays
Arrays are a kind of cross between lists and tuples with added muta-
bility. Like lists, they can have an arbitrary number of elements all of
the same type. Unlike lists (but like tuples), they cannot be extended
in size; there is no cons equivalent for arrays. Finally, each element of
an array can be individually indexed and updated. An example may
indicate the use of arrays:
Here, we’ve created an array of five elements, each the square of its
index. We update the third element to be 0, and examine the result,
which now has a 0 in the appropriate location.
We’ve used a new operator here, the binary sequencing operator (;),
which is a bit like the pair operator (,) in that it evaluates its left and
right arguments, except that the sequencing operator returns the
value only of the second.5 But then what could possibly be the point 5
You can think of P ; Q as being
syntactic sugar for let () = P in Q.
of evaluating the first argument? Since the argument isn’t used for
its value, it must be of interest for its side effects. That is the case in
this example; the expression gctr := !gctr + 1 has the side effect
of updating the counter to a new value, its old value (retrieved with
!gctr) plus one.6 Since the sequencing operator ignores the value 6
This part of the bump function that
does the actual incrementing of an
returned by its first argument, it requires that argument to be of type
int ref is a common enough activity
unit, the type for expressions with no useful value.7 that OCaml provides a function incr
We can test it out. : int ref -> unit in the Stdlib
library for just this purpose. It works as
# bump () ;; if implemented by
- : int = 1
let incr (r : int ref) : unit =
# bump () ;; r := !r + 1 ;;
- : int = 2
We could therefore have substituted
# bump () ;; incr gctr as the second line of the
- : int = 3 bump function.
7
Sometimes, you may want to sequence
Again, you see the hallmark of impure code – the same expression in an expression that returns a value other
the same context evaluates to different values. The change between than (). The ignore function of type ’a
-> unit in Stdlib comes in handy in
invocations happens because of the side effects of the earlier calls to
such cases.
bump. We can see evidence of the side effects also in the value of the
counter, which is globally visible.
# !gctr ;;
- : int = 3
To eliminate this abuse we’d like to avoid a global variable for the
counter. We’ve seen this kind of information hiding before – in the use
of local variables within functions, and in the use of signatures to hide
auxiliary values and functions from users of modules, all instances of
the edict of compartmentalization. But in the context of assignment,
making gctr a local variable (we’ll call it ctr) requires some thought. A
naive approach doesn’t work:
# let bump () =
# let ctr = ref 0 in
# ctr := !ctr + 1;
# !ctr ;;
val bump : unit -> int = <fun>
Exercise 171
What goes wrong with this definition? Try using it a few times and see what happens.
The problem: This code establishes the counter variable ctr upon
application of bump, and establishes a new such variable at each such
application. Instead, we want to define ctr just once, upon the defini-
tion of bump, and not its applications.
In this case, the compact notation for function definition, which
conflates the defining of the function and its naming, is doing us a
disservice. Fortunately, we aren’t obligated to use that syntactic sugar.
We can use the desugared version:
let bump =
fun () ->
ctr := !ctr + 1;
!ctr ;;
Now the naming (first line) and the function definition (second line
and following) are separate. We want the definition of ctr to outscope
the function definition but fall within the local scope of its naming:
# let bump =
# let ctr = ref 0 in
# fun () ->
# ctr := !ctr + 1;
# !ctr ;;
val bump : unit -> int = <fun>
The function is defined within the scope of – and therefore can access
and modify – a local variable ctr whose scope is only that function.
This definition operates as before to deliver incremented integers:
# bump () ;;
- : int = 1
# bump () ;;
- : int = 2
# bump () ;;
- : int = 3
282 PROGRAMMING WELL
but access to the counter variable is available only within the function,
as it should be, and not outside of it:
# !ctr ;;
Line 1, characters 1-4:
1 | !ctr ;;
^^^
Error: Unbound value ctr
Hint: Did you mean gctr?
Try to answer the questions below about the status of the various variables being defined
before typing them into the R E P L yourself.
1. After line 1, what is the type of p?
2. After line 2, what is the type of r?
3. After line 3, which of the following statements are true?
(a) p and s have the same type
(b) r and s have the same type
(c) p and s have the same value (in the sense that p = s would be true)
(d) r and s have the same value (in the sense that r = s would be true)
4. After line 6, what is the value of t?
5. After line 9, what is the value of t?
We can compute the length of such a list using the usual recursive
definition. We try
but this goes south in trying to calculate the recursive length of the tail
tl. Of course, tl isn’t an ’a mlist; it’s a reference to one. The fix is
easy:
Cons(1, r )
Box and arrow diagrams (Figure 15.3) help in figuring out what’s going s
on here.
Cons(2, )
Exercise 173 t
Write functions mhead and mtail that extract the head and the (dereferenced) tail from a Figure 15.3: Pictorial representation of
mutable list. For example, (top) the state of memory after building
# mhead t ;; some mutable list structures, and
- : int = 2 (bottom) updating with r := t. The
# mtail t ;;
nil has become garbage and the lists s
- : int mlist = Cons (1, {contents = Nil})
and t now have cycles in them.
284 PROGRAMMING WELL
Problem 174
For each of the following expressions, give its type and value, if any.
1. let a = Cons(2, ref (Cons(3, ref Nil))) ;;
let Cons(_h, t) = a in
let b = Cons(1, ref a) in
t := b;
mhead (mtail (mtail b)) ;;
Because the lists are mutable, we can modify the tail of s (equiva-
lently, r) to point to t.
# r := t ;;
- : unit = ()
# length t ;;
Stack overflow during evaluation (looping recursion?).
You’ll notice that the requirement to handle cyclic lists dramatically increases the
complexity of implementing length. (Hint: Keep a list of sublists you’ve already visited
and check to see if you’ve already visited each sublist. What is a reasonable value to
return in that case?)
Problem 176
Define a function first that returns a list (immutable) of the first n elements of a
mutable list mlst:
Problem 177
Write code to define a mutable integer list alternating such that for all integers n, the
expression first n alternating returns a list of alternating 1s and 2s, for example,
# first 5 alternating ;;
- : int list = [1; 2; 1; 2; 1]
# first 8 alternating ;;
- : int list = [1; 2; 1; 2; 1; 2; 1; 2]
sig
type 'a queue
val empty_queue : 'a queue
val enqueue : 'a -> 'a queue -> 'a queue
val dequeue : 'a queue -> 'a * 'a queue
end
Each call to enqueue and dequeue returns a new queue, differing from
its argument queue in having an element added or removed.
In an imperative implementation of queues, the enqueuing and
dequeuing operations can and do mutate the data structure, so that
the operations don’t need to return an updated queue. The types for
the operations thus change accordingly. We’ll use the following IMP_-
QUEUE signature for imperative queues:
Here again, you see the sign of a side-effecting operation: the enqueue
operation returns a unit. Dually, to convert a procedure that modifies
its argument and returns a unit into a pure function, the standard
technique is to have the function return instead a modified copy of its
argument, leaving the original untouched. Indeed, when we generalize
the substitution semantics of Chapter 13 to handle state and state
change in Chapter 19, we will use just this technique of passing a
representation of the computation state as an argument and returning
a representation of the updated state as the return value.
Another subtlety introduced by the addition of mutability is the
type of the empty_queue value. In the functional signature, we had
empty_queue : ’a queue; the empty_queue value was an empty
queue. In the mutable signature, we have empty_queue : unit ->
’a queue; the empty_queue value is a function that returns a (new,
physically distinct) empty queue. Without this change, the empty_-
queue value would be “poisoned” as soon as something was inserted
in it, so that further references to empty_queue would see the modified
(non-empty) value. Instead, the empty_queue function can generate a
new empty queue each time it is called.
286 PROGRAMMING WELL
This is basically the same as the list implementation from Section 12.4,
but with the imperative signature. Nonetheless, internally the opera-
tions are still functional, and enqueuing an element requires time lin-
ear in the number of elements in the queue. (Recall from Section 14.5
that the functional append function (here invoked as Stdlib.(@)) is
linear.)
We’ll examine two methods for generating constant time implemen-
tations of an imperative queue.
Enqueuing simply places the element on the top of the rear stack.
module TwoStackImpQueue : IMP_QUEUE =
struct
type 'a queue = {front : 'a list ref;
revrear : 'a list ref}
let empty_queue () =
{front = ref []; revrear = ref []}
let enqueue elt q =
q.revrear := elt :: !(q.revrear)
...
Exercise 178
An alternative is to use mutable record fields, so that the queue type would be
type 'a queue = {mutable front : 'a list;
mutable revrear : 'a list}
Reimplement the TwoStackImpQueue module using this type for the queue implementa-
tion.
288 PROGRAMMING WELL
To allow for manipulation of both the head of the queue (where en-
queuing happens) and the tail (where dequeuing happens), a final
implementation uses mutable lists. The queue type
q.rear := !tl)
| Nil -> (assert (!(q.front) = Nil);
q.front := Cons(elt, ref Nil);
q.rear := !(q.front))
...
Finally, dequeuing involves moving the front pointer to the next ele-
ment in the list, and updating the rear to Nil if the last element was
dequeued and the queue is now empty.
(* An empty dictionary *)
val empty : dict
(* Returns as an option the value associated with the
provided key. If the key is not in the dictionary,
returns None. *)
val lookup : dict -> key -> value option
(* Returns true if and only if the key is in the
dictionary. *)
val member : dict -> key -> bool
(* Inserts a key-value pair into the dictionary. If the
key is already present, updates the key to have the
new value. *)
val insert : dict -> key -> value -> dict
(* Removes the key from the dictionary. If the key is
not present, returns the original dictionary. *)
val remove : dict -> key -> dict
end ;;
Let’s experiment:
# open IntStringHashtbl ;;
# let d = empty () ;;
val d : IntStringHashtbl.dict = <abstr>
# insert d 10 "ten" ;;
- : unit = ()
# insert d 9 "nine" ;;
- : unit = ()
# insert d 34 "34" ;;
- : unit = ()
# insert d 1000 "a thousand" ;;
- : unit = ()
# lookup d 10 ;;
- : IntStringHashtbl.value option = Some "ten"
# lookup d 9 ;;
- : IntStringHashtbl.value option = Some "nine"
# lookup d 34 ;;
- : IntStringHashtbl.value option = Some "34"
# lookup d 8 ;;
- : IntStringHashtbl.value option = None
# remove d 9 ;;
- : unit = ()
# lookup d 10 ;;
- : IntStringHashtbl.value option = Some "ten"
# lookup d 9 ;;
- : IntStringHashtbl.value option = None
# lookup d 34 ;;
- : IntStringHashtbl.value option = Some "34"
# lookup d 8 ;;
- : IntStringHashtbl.value option = None
Exercise 179
Complete the implementation by providing implementations of the remaining func-
tions lookup, member, insert, and remove.
Exercise 180
Improve the collision handling in the implementation by allowing the linear probing to
“wrap around” so that if it reaches the end of the array it keeps looking at the beginning
of the array.
Exercise 181
A problem with linear probing is that as collisions happen, contiguous blocks of the
array get filled up, so that further collisions tend to yield long searches to get past
these blocks for an empty location. Better is to use a method of rehashing that leaves
some gaps. A simple method to do so is QUA D R AT I C P R O B I N G : each probe increases
quadratically, adding 1, then 2, then 4, then 8, and so forth. Modify the implementation
so that it uses quadratic probing instead of linear probing.
15.7 Conclusion
while ⟨exprcondition ⟩ do
⟨exprbody ⟩
done
if the condition expression ⟨exprcondition ⟩ is true the first time it’s eval-
uated, it will remain so perpetually and the loop will never terminate.
Conversely, if the condition expression is false the first time it’s eval-
uated, it will remain so perpetually and the loop body will never be
evaluated. Similarly, the body expression ⟨exprbody ⟩ will always evalu-
ate to the same value, so what could possibly be the point of evaluating
it more than once?
In summary, procedural programming only makes sense in a lan-
guage with side effects, the kind of impure constructs (like variable
assignment) that we introduced in the previous chapter. You can see
this need in attempting to implement the length function in this pro-
cedural paradigm. Here is a sketch of a procedure for calculating the
length of a list:
We’ll need to establish the counter in such a way that its value can
change. Similarly, we’ll need to update the list each time the loop
body is executed. We’ll thus need both the counter and the list being
manipulated to be references, so that they can change. Putting all this
together, we get the following procedure for computing the length of a
list:
# let length_iter (lst : 'a list) : int =
# let counter = ref 0 in (* initialize the counter *)
# let lst_ref = ref lst in (* initialize the list *)
# while !lst_ref <> [] do (* while list not empty... *)
# incr counter; (* increment the counter *)
# lst_ref := List.tl !lst_ref (* drop element from list *)
# done;
# !counter ;; (* return the counter value *)
val length_iter : 'a list -> int = <fun>
# length_iter [1; 2; 3; 4; 5] ;;
- : int = 5
linearly growing stack of suspended calls. Figure 16.1: The nested stack of sus-
The iterative approach, on the other hand, needs no stack of sus- pended calls in evaluating a non-tail-
recursive length function.
pended computations. The single call to length_iter invokes the
while loop to iteratively increment the counter and drop elements
from the list. The computation is “flat”.
298 PROGRAMMING WELL
# length_iter very_long_list ;;
- : int = 1000000
# length very_long_list ;;
Stack overflow during evaluation (looping recursion?).
The profligate use of space for stack frames is not inherent in all purely
functional recursive computations however. Consider the following
purely functional method length_tr for implementing the length
calculation.
# length_tr very_long_list ;;
- : int = 1000000
LOOPS AND PROCEDURAL PROGRAMMING 299
This version doesn’t have the same problem. It’s easy to see why. For
the recursive length, the result of each call is a computation using the
result of the embedded call to length; that computation must there-
fore be suspended, and a stack frame must be allocated to store infor-
mation about that pending computation. But the result of each call to
the recursive length_plus is not just a computation using the result of
the embedded call to length_plus; it is the result of that nested call.
We don’t need to store any information about a suspended computa-
tion – no need to allocate a stack frame – because the embedded call
result is all that is needed.
Recursive programs written in this way, in which the recursive invo-
cation is the result of the invoking call, are deemed TA I L R E C U R S I V E
(hence the _tr in the function’s name). Tail-recursive functions need
not use a stack to keep track of suspended computations. Program-
ming language implementations that take advantage of this possibility
by not allocating a stack frame to tail-recursive applications are said to
perform TA I L - R E C U R S I O N O P T I M I Z AT I O N , effectively turning the re-
cursion into a corresponding iteration, and yielding the benefits of the
procedural iterative solution. The OCaml interpreter is such a language
implementation.
Thus, this putative advantage of loop-based procedures over recur-
sive functions – the ability to perform computations space-efficiently –
can often be replicated in functional style through careful tail-recursive
implementation where needed.
You’ll see discussion of this issue, for instance, in the description
of functions in the List library, which calls out those functions that
are not tail-recursive.1 For instance, the library function fold_left is 1
From the List library documentation:
“Some functions are flagged as not
implemented in a tail-recursive manner, so it can fold over very long
tail-recursive. A tail-recursive function
lists without running out of stack space. By contrast, the fold_right uses constant stack space, while a
implementation is not tail-recursive, so may not be appropriate when non-tail-recursive function uses stack
space proportional to the length of its
processing extremely long lists. list argument, which can be a problem
with very long lists. . . . The above
considerations can usually be ignored
16.3 Saving data structure space if your lists are not longer than about
10000 elements.”
# let rec map (fn : 'a -> 'b) (lst : 'a list) : 'b list =
# match lst with
# | [] -> []
# | hd :: tl -> fn hd :: map fn tl ;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
The result is a list with different values. Most notably, the result is a new
list. The original is unchanged.
# original ;;
- : int list = [1; 2; 3]
The functions cons and pair could be used to replace their built-in
counterparts for consing (::) and pairing (,) to track the number of
allocations required.
Problem 182
Implement the module Metered.
LOOPS AND PROCEDURAL PROGRAMMING 301
Problem 183
Reimplement the zip function of Section 10.3.2 using metered conses and pairs.
# Metered.reset () ;;
- : unit = ()
# zip [1; 2; 3; 4; 5] [5; 4; 3; 2; 1] ;;
- : (int * int) list = [(1, 5); (2, 4); (3, ...); ...]
# Metered.count () ;;
- : int = 10
We see the effect of the map this time not in the return value but in the
modified original array.
# original ;;
- : int array = [|2; 3; 4|]
Problem 184
Implement a metered version of quicksort, and experiment with how many allocations
are needed to sort lists of different lengths.
let sort (lt : 'a -> 'a -> bool) (arr : 'a array) : unit =
Finally, to sort the entire array, we can sort the region between the
leftmost and rightmost indices
sort_region 0 ((Array.length arr) - 1)
(* process each element, moving those less than the Figure 16.3: (continued) Implementa-
pivot to before the border *) tion of an in-place quicksort.
while !current <= right do
if lt arr.(!current) pivot_val then
begin
(* current should be left of pivot *)
swap !current !border; (* swap into place *)
incr border (* move border right to make room *)
end;
incr current
done;
invariants concerning the left, right, current, and border indices and
the elements in the various subregions. In part the length is a result
of considerably more documentation in the implementation, but that
is not a coincidence. The implementation requires this additional
documentation to be remotely as understandable as the pure version.
(Even still, an understanding of the in-place version is arguably more
complex. It’s hard to imagine understanding how the partition func-
tion works without manually “playing computer” on some examples to
verify the procedure.)
The payoff is that the in-place version needs to allocate only a tiny
amount of space beyond the storage in the various stack frames for the
function applications – just the storage for the current and border
elements. Is the cost in code complexity and opaqueness worth it?
That depends on the application. If sorting huge amounts of data is
necessary, the reduction in space may be needed.
Problem 185
A completely in-place version of mergesort that uses only a fixed amount of extra space
turns out to be quite tricky to implement. However, a version that uses only a single
extra array is possible, and still more space-efficient than the pure version described in
Section 14.2. Implement a version of mergesort that uses a single extra array as “scratch
space” for use while merging. To sort a region, we sort the left and right subregions
recursively, then merge the two into the scratch array, and finally copy the merged region
back into the main array.
17
Infinite data structures and lazy programming
P Q⇓
RRR P ⇓ fun x -> B
RRR
RRR Q ⇓ v (R app )
RRR Q
RRR B [x ↦ v ] ⇓ v
R Q B
⇓ vB
let x = D in B ⇓
D ⇓ vD
∣ (R let )
B [x ↦ v D ] ⇓ v B
⇓ vB
its widest use in the Haskell language named after Haskell Curry (Fig-
ure 6.2).
We’ll make use of lazy evaluation in perhaps the most counter-
intuitive application, the creation and manipulation of infinite data
structures. We start with the stream, a kind of infinite list.
17.2 Streams
It’s all well and good to have streams and functions over them,
but how are we to build one? It looks like we have a chicken and egg
problem, requiring a stream in order to create one. Nonetheless, we
press on, building a stream whose head is the integer 1. We start with
let ones = Cons (1, ...) ;;
We need to fill in the ... with an int stream, but where are we to find
one? How about the int stream named ones itself?
# let ones = Cons (1, ones) ;;
Line 1, characters 20-24:
1 | let ones = Cons (1, ones) ;;
^^^^
Error: Unbound value ones
Of course, that doesn’t work, because the name ones isn’t itself avail-
able in the definition. That requires a let rec.
# let rec ones = Cons (1, ones) ;;
val ones : int stream = Cons (1, <cycle>)
Its head is one, as is the head of its tail, and the head of the tail of the
tail. It seems to be an infinite sequence of ones!
What is going on here? How does the implementation make this
possible? Under the hood, the components of an algebraic data type
have implicit pointers to their values. When we define ones as above,
OCaml allocates space for the cons without initializing it (yet) and
connects the name ones to it. It then initializes the contents of the
cons, the head and tail, a pair of hidden pointers. The head pointer
points to the value 1, and the tail points to the cons itself. This explains
where the notation <cycle> comes from in the R E P L printing out the
value. In any case, the details of how this behavior is implemented isn’t
necessary to make good use of it.
Not all such cyclic definitions are well defined however. Consider
this definition of an integer x:
# let rec x = 1 + x ;;
Line 1, characters 12-17:
1 | let rec x = 1 + x ;;
^^^^^
Error: This kind of expression is not allowed as right-hand side of
`let rec'
We can allocate space for the integer and name it x, but when it comes
to initializing it, we need more than just a pointer to x; we need its
value. But that isn’t yet defined, so the whole process fails and we get
an error message.
# let rec smap (f : 'a -> 'b) (s : 'a stream) : ('b stream) =
# match s with
# | Cons (hd, tl) -> Cons (f hd, smap f tl) ;;
val smap : ('a -> 'b) -> 'a stream -> 'b stream = <fun>
# let rec smap (f : 'a -> 'b) (s : 'a stream) : ('b stream) =
# Cons (f (head s), smap f (tail s)) ;;
val smap : ('a -> 'b) -> 'a stream -> 'b stream = <fun>
Now, we map the successor function over the stream of ones to form a
stream of twos.
Of course, that doesn’t work at all. We’re asking OCaml to add one to
each element in an infinite sequence of ones. Luckily, smap isn’t tail
recursive, so we blow the stack, instead of just hanging in an infinite
loop. This behavior makes streams as currently implemented less
than useful since there’s little we can do to them without getting into
trouble. If only the system were less eager about doing all those infinite
number of operations, doing them only if it “needed to”.
The problem is that when calculating the result of the map, we need
to generate (and cons together) both the head of the list (f (head s))
and the tail of the list (smap f (tail s)). But the tail already involves
calling smap.
Why isn’t this a problem in calling regular recursive functions, like
List.map? In that case, there’s a base case that is eventually called.
Why isn’t this a problem in defining regular recursive functions?
Why is there no problem in defining, say,
let rec fact n =
if n = 0 then 1
else n * fact (pred n) ;;
The name fact can be associated with a function that uses it because
functions are values. The parts inside are not further evaluated, at least
not until the function is called. In essence, a function delays the latent
computation in its body until it is applied to its argument.
We can take advantage of that in our definition of streams by using
functions to perform computations lazily. We achieve laziness by
wrapping the computation in a function delaying the computation
until such time as we need the value. We can then force the value by
applying the function.
To achieve the delay of computation, we’ll take a stream not to
be a cons as before, but a delayed cons, a function from unit to the
cons. Other functions that need access to the components of the de-
layed cons can force it as needed. We need a new type definition for
streams, which will make use of a simultaneously defined auxiliary
type stream_internal:1 1
The and connective allows mutually re-
cursive type definitions. Unfortunately,
# type 'a stream_internal = Cons of 'a * 'a stream OCaml doesn’t allow direct definition of
# and 'a stream = unit -> 'a stream_internal ;; nested types, like
type 'a stream_internal = Cons of 'a * 'a stream type 'a stream = unit -> (Cons of 'a * 'a stream)
and 'a stream = unit -> 'a stream_internal
Notice that it returns a delayed cons, that is, a function which, when
applied to a unit, returns the cons.
We need to redefine the functions accordingly to take and return
these new lazy streams. In particular, head and tail now force their
argument by applying it to unit.
# let head (s : 'a stream) : 'a =
# match s () with
# | Cons (hd, _tl) -> hd ;;
val head : 'a stream -> 'a = <fun>
# let rec smap (f : 'a -> 'b) (s : 'a stream) : ('b stream) =
# fun () -> Cons (f (head s), smap f (tail s)) ;;
val smap : ('a -> 'b) -> 'a stream -> 'b stream = <fun>
The smap function now returns a lazy stream, a function, so that the
recursive call to smap isn’t immediately evaluated (as it was in the
old definition). Only when the cons is needed (as in the head or tail
functions) is the function applied and the cons constructed. That cons
itself has a stream as its tail, but that stream is also delayed.
Now, finally, we can map the successor function over the infinite
stream of ones to form an infinite stream of twos.
# let twos = smap succ ones ;;
val twos : int stream = <fun>
# head twos ;;
- : int = 2
# head (tail twos) ;;
- : int = 2
# head (tail (tail twos)) ;;
- : int = 2
# first 10 twos ;;
- : int list = [2; 2; 2; 2; 2; 2; 2; 2; 2; 2]
Every time we want access to the head or tail of the stream, we need
to rerun the function. In a computation like the Fibonacci defini-
tion above, that means that every time we ask for the n-th Fibonacci
number, we recalculate all the previous ones – more than once. But
if the value being forced is pure, without side effects, there’s no rea-
son to recompute it. We should be able to avoid the recomputation
by remembering its value the first time it’s computed, and using the
remembered value from then on. The term of art for this technique is
2
M E M O I Z AT I O N . 2
Not “memorization”. For unknown
reasons, computer scientists have
We’ll encapsulate this idea in a new abstraction called a T H U N K ,
settled on this bastardized form of the
essentially a delayed computation that stores its value upon being word.
forced. We implement a thunk as a mutable value (a reference) that
can be in one of two states: not yet evaluated or previously evaluated.
The type definition is thus structured with two alternatives.
Notice that in the unevaluated state, the thunk stores a delayed value
of type ’a. Once evaluated, it stores an immediate value of type ’a.
When we need to access the actual value encapsulated in a thunk,
we’ll use the force function. If the thunk has been forced before and
thus evaluated, we simply retrieve the value. Otherwise, we compute
the value, remember it by changing the state of the thunk to be evalu-
ated, and return the value.
Here’s a thunk for a computation of, say, factorial of 15. To make the
timing clearer, we’ll give it a side effect of printing a short message.
# let fact15 =
# ref (Unevaluated (fun () ->
# print_endline "evaluating 15!";
# fact 15)) ;;
val fact15 : int thunk_internal ref = {contents = Unevaluated
<fun>}
Now that the value has been forced, it is remembered in the thunk
and can be returned without recomputation. You can tell that no
recomputation occurs because the printing side effect doesn’t happen,
and the computation takes orders of magnitude less time.
# fact15 ;;
- : int thunk_internal ref = {contents = Evaluated 1307674368000}
# Absbook.call_reporting_time force fact15 ;;
time (msecs): 0.000954
- : int = 1307674368000
Functions on streams will need to force the stream values. Here, for
instance, is the head function:
318 PROGRAMMING WELL
Exercise 186
Rewrite tail, smap, smap2, and first to use the Lazy module.
The Fibonacci sequence can now be reconstructed. It runs hun-
dreds of times faster than the non-memoized version in Section 17.2.1:
# let rec fibs =
# lazy (Cons (0,
# lazy (Cons (1,
# smap2 (+) fibs (tail fibs))))) ;;
val fibs : int stream = <lazy>
# Absbook.call_reporting_time (first 10) fibs ;;
time (msecs): 0.006199
- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34]
π 1 1 1
= 1− + − +⋯
4 3 5 7
and
4 4 4
π = 4− + − +⋯ . Figure 17.4: The arctangent of 1, that
3 5 7 is, the angle whose ratio of opposite to
We can thus approximate π by adding up the terms in this infinite adjacent side is 1, is a 45 degree angle,
or π 4
in radians.
stream of numbers.
We start with a function to convert a stream of integers to a stream
of floats.
# let to_float = smap float_of_int ;;
val to_float : int stream -> float stream = <fun>
# let alt_signs =
# smap (fun x -> if x mod 2 = 0 then 1 else -1) nats ;;
val alt_signs : int stream = <lazy>
# first 5 odds ;;
- : int list = [1; 3; 5; 7; 9]
# first 5 alt_signs ;;
- : int list = [1; -1; 1; -1; 1]
# first 5 pi_stream ;;
- : float list =
[4.; -1.33333333333333326; 0.8; -0.571428571428571397;
0.44444444444444442]
# let pi_approx n =
# List.fold_left ( +. ) 0.0 (first n pi_stream) ;;
val pi_approx : int -> float = <fun> The given sequence
# pi_approx 10 ;; 1 2 3 4 5 6 7
- : float = 3.04183961892940324 ...
# pi_approx 100 ;; . . . and its partial sums
- : float = 3.13159290355855369 1 3 6 10 15 21 28
# pi_approx 1000 ;; ...
Prepend a zero to the partial sums
- : float = 3.14059265383979413
0 1 3 6 10 15 21 28
# pi_approx 10000 ;;
...
- : float = 3.14149265359003449 . . . plus the original sequence
# pi_approx 100000 ;; 1 2 3 4 5 6 7 8
- : float = 3.14158265358971978 ...
. . . yields the partial sums
After 100,000 terms, we have a pretty good approximation of π, good to 1 3 6 10 15 21 28 36
about four decimal places. ...
Of course, this technique of partial sums isn’t in the spirit of infinite Figure 17.5: Creating an infinite stream
of partial sums of a given stream, in this
streams. Better would be to generate an infinite stream of all of the
case, the stream of positive integers.
partial sums. Figure 17.5 gives a recipe for generating a stream of We prepend a zero to the sequence’s
partial sums from a given stream. Starting with the stream, we take its partial sums and add in the original
sequence to generate the sequence
partial sums (!) and prepend a zero. Adding the original stream and the of partial sums. Only by virtue of lazy
prepended partial sums stream yields. . . the partial sums stream. This computation can this possibly work.
technique, implemented as a function over streams, is:
320 PROGRAMMING WELL
We can now search for a value accurate to within any number of digits
we desire:
Exercise 187
As mentioned in Exercise 32, the ratios of successive numbers in the Fibonacci sequence
approach the golden ratio (1.61803 . . .). Show this by generating a stream of ratios of
successive Fibonacci numbers and use it to calculate the golden ratio within 0.000001.
output is false. The not gate is a boolean device with a single input; its
output is true when its input is false and vice versa.
In this problem, you’ll generate code for modeling boolean circuits.
The inputs and outputs will be modeled as lazy boolean streams.
Let’s start with an infinite stream of false values.
Exercise 188
Define a value falses to be an infinite stream of the boolean value false.
Exercise 189
What is the type of falses?
Exercise 190
A useful function is the trueat function. The expression trueat n generates a stream of
values that are all false except for a single true at index n:
# first 5 (trueat 1) ;;
- : bool list = [false; true; false; false; false]
Exercise 191
Define a function circnot : bool stream -> bool stream to represent the not gate.
It should have the following behavior:
# first 5 (circnot (trueat 1)) ;;
- : bool list = [true; false; true; true; true]
Exercise 192
Define a function circand to represent the and gate. It should have the following
behavior:
# first 5 (circand (circnot (trueat 1)) (circnot (trueat 3))) ;;
- : bool list = [true; false; true; false; true]
Exercise 193
Succinctly define a function circnand using the functions above to represent the nand
gate. It should have the following behavior:
# first 5 (circnand falses (trueat 3)) ;;
- : bool list = [true; true; true; true; true]
# first 5 (circnand (trueat 3) (trueat 3)) ;;
- : bool list = [true; true; true; false; true]
With the additional tools of algebraic data types and lazy evaluation,
we can put together a more elegant framework for unit testing. Lazy
evaluation in particular is useful here, since a unit test is nothing other
than an expression to be evaluated for its truth at some later time
when the tests are run. Algebraic data types are useful in a couple of
ways, first to package together the components of a test and second to
express the alternative ways that a test can come out.
322 PROGRAMMING WELL
# type status =
# | Passed
# | Failed
# | Raised_exn of string
(* string describing exn *)
# | Timed_out of int (* timeout in seconds *) ;;
type status = Passed | Failed | Raised_exn of string | Timed_out of
int
# type test =
# { label : string;
# condition : bool Lazy.t;
# time : int } ;;
type test = { label : string; condition : bool Lazy.t; time : int;
}
Notice that the condition of the test is a lazy boolean, so that the con-
dition will not be evaluated until the test is run.
To construct a test, we provide a function that packages together the
components.3 3
We make use of an optional argument
for the time, which defaults to five
# (* test ?time label condition -- Returns a test with the seconds if not provided. For the inter-
# given label and condition, with optional timeout time ested, details of optional arguments are
# in seconds. *) discussed here.
# let test ?(time=5) label condition =
# {label; condition; time} ;;
val test : ?time:int -> string -> bool Lazy.t -> test = <fun>
But what if the test raises an exception? We’ll evaluate the test condi-
tion in a try ⟨⟩ with ⟨⟩ to deal with this case.
# a Timeout exception. *)
# exception Timeout ;;
exception Timeout
# let sigalrm_handler =
# let old_behavior =
# let reset_sigalrm () =
# reset_sigalrm () ; res ;;
# report tests ;;
should fail: failed
should pass: passed
should time out: timed out after 5 seconds
should raise exception: raised Failure("nth")
- : unit = () Figure 17.7: Peter Landin (1930–2009),
developer of many innovative ideas
in programming languages, including
17.7 A brief history of laziness the roots of lazy programming. His
influence transcended his role as a
The idea of lazy computation probably starts with Peter Landin (Fig- computer scientist, especially in his
active support of gay rights.
ure 17.7). He observed “a relationship between lists and functions”:
In this relationship a nonnull list L is mirrored by a none-adic function S
that produces a 2-list consisting of (1) the head of L, and (2) the function
mirroring the tail of L. . . . This correspondence serves two related pur-
poses. It enables us to perform operations on lists (such as generating
326 PROGRAMMING WELL
The idea of a “function mirroring the tail of” a list is exactly the delay-
ing of the tail computation that we’ve seen in the stream data type.
Landin is notable for many other ideas of great currency. For in-
stance, he invented the term “syntactic sugar” for the addition of
extra concrete syntax to abbreviate some useful but otherwise com-
plicated abstract syntax. His 1966 paper “The next 700 programming
languages” (Landin, 1966) introduced several innovative ideas in-
cluding the “offside rule” of concrete syntax, allowing the indentation
pattern of a program to indicate its structure. Python is typically noted
for making use of this Landin innovation. Indeed, the ISWIM language
that Landin described in this paper is arguably the most influential
programming language that no one ever programmed in.
Following Landin’s observation, Wadsworth proposed the lazy
lambda calculus in 1971, and Friedman and Wise published an article
proposing that “Cons should not evaluate its arguments” in 1976. The
first programming language to specify lazy evaluation as the evaluation
regime was Burstall’s Hope language (which also introduced the idea,
found in nascent form in ISWIM, of algebraic data types). A series of
lazy languages followed, most notably Miranda, but the lazy program-
ming community came together to converge on the now canonical lazy
language Haskell, named after Haskell Curry.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
You are allowed (and encouraged) to work with a partner on this problem
set. You are also allowed to work alone, if you prefer. See the course
document “Problem set procedures” for further information on working
with partners on problem sets.
In this problem set you will work with two new ideas: First, we pro-
vide a bit of practice with imperative programming, emphasizing mu-
table data structures and the interaction between assignment and lex-
ical scoping. Since this style of programming is probably most familar
to you, this portion of the problem set is brief. Second, we introduce
I N F I N I T E D ATA S T RU C T U R E S A N D L A Z Y P R O G R A M M I N G 327
lazy programming and its use in modeling infinite data structures. This
part of the problem set is more extensive, and culminates in a project
to generate infinite streams of music.
1 2 3 2 2
(a) (b)
Problem 194
Write a function has_cycle that returns true if and only if a mutable list has a cycle.
Your function must not alter the original mutable list, and it must terminate eventually.
Think about how you will know if a node that you are visiting has been seen before.
Testing whether the reference a_ref points to b can be done with (!a_ref) == b. The
(==) function tests equality at the level of memory location rather than value.
Problem 195
Write a function flatten that takes a mutable list and removes any cycle in it destruc-
tively by removing backward links. This means that the data structure should be changed
in-place, such that the list passed as an argument itself is altered if necessary. Note that
this is very different from the functional programming approach that we have been using
up to this point, where functions might return an altered copy of a data structure. Sup-
pose you pass in the mutable list from Figure 17.8(b); flatten should alter it such that
it looks like Figure 17.8(a), and then return unit. If you are unsure how to destructively
alter a mutable list, take a look at reflist in refs_test.ml.
Problem 196
Write mlength, which finds the number of elements in a mutable list. This should always
terminate, even if the list has a cycle. For example, both Figures 17.8(a) and (b) have
length 4. The mlength function must be nondestructive, that is, the original mutable list
should not change.
Problem 197
Challenge problem: It’s possible to complete Problem 194 in such a way that it doesn’t
use any additional space other than that taken up by the list passed as an argument.
Attempt this only if you’ve finished the rest of the assignment and are up for a challenge.
In this section you’ll gain practice with lazy evaluation using the
OCaml Lazy module through problems with infinite streams and
infinite trees.
328 PROGRAMMING WELL
# first 5 pi_sums ;;
- : float list =
[4.; 2.66666666666666696; 3.46666666666666679; 2.89523809523809561;
3.33968253968254025]
The method works, but converges quite slowly. It takes some 200 terms
in the expansion to get within 0.01 of π. In this section of the problem
set, you will use a technique called series acceleration to speed up
the process of converging on a value. A simple method is to average
adjacent elements in the approximation stream. The necessary code
for you to use and modify can be found in the file streamstrees.ml.
Problem 198
Write a function average that takes a float stream and returns a stream of floats each
of which is the average of adjacent values in the input stream. For example:
# first 5 (average (to_float nats)) ;;
- : float list = [0.5; 1.5; 2.5; 3.5; 4.5]
You should then be able to define a stream pi_avgs of the averages of the partial sums
in pi_sums. How many steps does it take to get within 0.01 of π using pi_avgs instead of
pi_sums?
(s n − s n −1 )2
s n′ = s n −
s n − 2s n −1 + s n −2
Problem 199
Implement a function aitken that applies Aitken’s method to a float stream returning
the resulting float stream.
Problem 200
Try the various methods to compute approximations of π and fill out the table in the
streamstrees.ml file with what you find.
I N F I N I T E D ATA S T RU C T U R E S A N D L A Z Y P R O G R A M M I N G 329
Infinite trees Just as streams are a lazy form of list, we can have a lazy
form of trees. In the definition below, each node in a lazy tree of type
’a tree holds a value of some type ’a, and a (conventional, finite) list
of one or more (lazy) child trees.
Problem 201
Complete the implementation by writing functions node, children, print_depth, tmap,
tmap2, bfenumerate, onest, levels, and tree_nats as described in streamstrees.ml.
We recommend implementing them in that order.
In this part, you will explore a fun application of streams: music. Be-
fore you begin, if you’re not already familiar with basic music notation
and theory, you should learn some of these concepts. Numerous on-
line tutorials exist, such as this one, though if you’re short on time, the
following introduction should be sufficient for this assignment.
Data types for musical objects The top of music.ml provides simple
definitions for musical data types. The type pitch is a tuple of a p,
which has one constructor for each of the 12 pitch names, and an
integer representing the octave. (Note that our definition includes only
flats and not sharps. We apologize if this offends any music theorists.)
There is also a data type representing musical objects, obj. An obj can
330 PROGRAMMING WELL
For example, if one stream has a start at time 0, and a stop at time 0.5 after that, and
the other stream has a start at time 0.25 and a stop at time 0.5 after that, the combined
stream should have a start at time 0, the second start at 0.25 after that, the first stop
at 0.25 after that, and the second stop at 0.25 after that. This will require some careful
thought to update the time differences of the event that is not chosen.
Problem 204
Write a function transpose that takes an event stream and transposes it (moves each
pitch up by a given number of half_steps.) For example, a C transposed up 7 half-steps
is a G. A C transposed up 12 half-steps is the C in the next octave up. Use the helper
function transpose_pitch, which transposes a single pitch value by the correct number
of half-steps.
Define a stream canon that represents this piece. Use the functions shift_start and
pair, and the streams bass and melody. When you’re done, uncomment the line below
the definition and compile and run the code. It will produce a file canon.mid which,
if you’ve done the problem correctly, will contain the first eight measures of the piece.
(We export 176 events since these eight measures should contain 88 notes; you could
increase this number if you want to hear more music, though it won’t be true to the
original.) If it sounds wrong, it probably is. If it sounds right, either it is or you’re a
brilliant composer. (Note: As this is not a music class, brilliant composers will not earn
extra points on this question.)
Problem 206
Challenge problem: There’s lots of opportunity for extending your system. If you want,
try implementing other functions for manipulating event streams. Maybe one that
increases or decreases the timescale, or one that doubles every note, or increases or
decreases the volume. See what kind of music you can create. We’ve given you some
more interesting streams of music to play around with if you’d like, named part1
through part4. They’re all the same length, 2.25 measures.
18
Extension and object-oriented programming
But new widgets are being invented all the time. Every time a new
widget type is added, we’d have to change every one of these functions.
Instead, we might want to organize the code a different way:
This way, adding a new widget doesn’t affect any of the existing
ones. The changes are localized, and therefore likely to be much more
reliably added. We are carving the software at its joints, following the
edict of decomposition.
This latter approach to code organization, organizing by “object”
334 PROGRAMMING WELL
We might want data types for the individual kinds of graphical ele-
ments – rectangles, circles, squares – each with its own parameters
specifying pertinent positions, sizes, and the like:
# type rect = {rect_pos : point;
# rect_width : int; rect_height : int} ;;
type rect = { rect_pos : point; rect_width : int; rect_height :
int; }
# type circle = {circle_pos : point; circle_radius : int} ;; Figure 18.3: Alan Kay, Adele Goldberg,
type circle = { circle_pos : point; circle_radius : int; } and Dan Ingalls, developers of the influ-
ential Smalltalk language, a pioneering
# type square = {square_pos : point; square_width : int} ;; object-oriented language, with an inno-
type square = { square_pos : point; square_width : int; } vative user interface based on graphical
widgets and direct manipulation.
We can think of a scene as being composed of a set of these display
elements:
# type display_elt =
# | Rect of rect
E X T E N S I O N A N D O B J E C T- O R I E N T E D P R O G R A M M I N G 335
# | Circle of circle
# | Square of square ;;
type display_elt = Rect of rect | Circle of circle | Square of
square
# module G = Graphics ;;
module G = Graphics
# let test_scene =
# [ Rect {rect_pos = {x = 0; y = 20};
336 PROGRAMMING WELL
# draw_scene test_scene ;;
- : unit = ()
# type display_elt =
# | Rect of rect
# | Circle of circle
# | Square of square
# | Text of text ;;
type display_elt =
Rect of rect
| Circle of circle
| Square of square
| Text of text
Then rectangles, circles, squares, and texts are just ways of building
display elements with that drawing functionality.
Take rectangles for example. A rectangle is a display_elt whose
draw function displays a rectangle. We can establish a rect function
that builds such a display element given its initial parameters – posi-
tion, width, and height:
# let rect (p : point) (w : int) (h : int) : display_elt =
# { draw = fun () ->
# G.set_color G.black ;
# G.fill_rect (p.x - w/2) (p.y - h/2) w h } ;;
val rect : point -> int -> int -> display_elt = <fun>
Now to draw a display element, we just extract the draw function and
call it. The display element data object knows how to draw itself.
# let draw (d : display_elt) = d.draw () ;;
val draw : display_elt -> unit = <fun>
# type display_elt =
# { draw : unit -> unit;
# set_pos : point -> unit;
# get_pos : unit -> point;
# set_color : G.color -> unit;
# get_color : unit -> G.color } ;;
type display_elt = {
draw : unit -> unit;
set_pos : point -> unit;
get_pos : unit -> point;
set_color : G.color -> unit;
get_color : unit -> G.color;
}
The scoping is crucial. The definitions of pos and color are within
the scope of the circle function. Thus, new references are generated
each time circle is invoked and are accessible only to the record
structure (the object) created by that invocation.2 Similarly, we’ll want 2
Recall the similar idea of local, other-
wise inaccessible, persistent, mutable
a function to create rectangles and text boxes, each with its own state
state first introduced in the bump func-
and functionality as specified by the display_elt type. tion from Section 15.3, and reproduced
here:
# let rect (p : point) (w : int) (h : int) : display_elt =
# let bump =
# let pos = ref p in
# let ctr = ref 0 in
# let color = ref G.black in
# fun () ->
# { draw = (fun () -> # ctr := !ctr + 1;
# G.set_color (!color); # !ctr ;;
# G.fill_rect ((!pos).x - w/2) ((!pos).y - h/2) val bump : unit -> int = <fun>
# w h);
# set_pos = (fun p -> pos := p);
340 PROGRAMMING WELL
G.set_color (!color);
G.set_color (!color); G.set_color (!color)
G.moveto (!pos).x
G.fill_rect (!pos).x G.fill_circle (!pos).x
draw (!pos).y;
(!pos).y w h (!pos).y r
G.draw_string s
Which is the better approach? The edict of decomposition appeals Table 18.1: The matrix of functionality
(rows) and object classes (columns)
to cutting up software at its joints. Which of row or column constitutes for the display elements example.
the natural joints will vary from case to case. It is thus a fundamental The code can be organized by row –
function-oriented – or by column –
object-oriented.
E X T E N S I O N A N D O B J E C T- O R I E N T E D P R O G R A M M I N G 341
Now we can use these to create and draw some display elements. We
create a new circle,
(a)
# let _ = G.open_graph "";
# G.clear_graph ;;
- : unit -> unit = <fun>
# let b = new circle {x = 100; y = 100} 40 ;;
val b : circle = <obj>
# let _ = b#draw ;;
- : unit = ()
(Figure 18.5(b)).
18.4 Inheritance
(c)
The code we’ve developed so far violates the edict of irredundancy.
The implementations of the circle and rect classes, for instance, are
almost identical, differing only in the arguments of the class and the
details of the draw method.
To capture the commonality, the object-oriented paradigm allows
for definition of a class expressing the common aspects, from which
both of the classes can I N H E R I T their behaviors. We refer to the class
Figure 18.5: A circle appears (a) and
(or class type) that is being inherited from as the S U P E R C L A S S and the disappears (b). It moves and reappears
inheriting class as the S U B C L A S S . with a changed color (c).
We’ll define a shape superclass that can handle the position and
color aspects of the more specific classes. Its class type is given by
344 PROGRAMMING WELL
Notice that the new shape_elt signature provides access to the four
methods, but not directly to the instance variables used to implement
those methods. The only access to those instance variables will be
through the methods, an instance of the edict of compartmentaliza-
tion that seems appropriate.
The display_elt class type can inherit the methods from shape_-
elt, adding just the additional draw method.
The rect and circle subclasses can inherit much of their behavior
from the shape superclass, just adding their own draw methods. How-
ever, without the ability to refer directly to the instance variables, the
draw method will need to call its own methods for getting and setting
the position and color. We can add a variable to name the object itself,
by adding a parenthesized name after the object keyword. Although
any name can be used, by convention, we use this or self. We can
then invoke the methods from the shape superclass with, for instance,
this#get_color.
Notice how the inherited shape class is provided the position argu-
ment p so its instance variables and methods can be set up properly.
Using inheritance, a square class can be implemented with a single
inheritance from the rect class, merely by specifying that the width
and height of the inherited rectangle are the same:
# class square (p : point) (w : int) : display_elt =
# object
# inherit rect p w w
# end ;;
class square : point -> int -> display_elt
Exercise 207
Define a class text : point -> string -> display_elt for placing a string of text at
a given point position on the canvas. (You’ll need the Graphics.draw_string function
for this.)
18.4.1 Overriding
rectangles (rather than the filled rectangles of the rect class) simply by
overriding the draw method:
18.5 Subtyping 3
The type of the scene is displayed not,
as one might expect, as display_elt
Back in Section 18.1, we defined a scene as a set of drawable ele- list but as border_rect list. OCaml
uses class names, not class type names,
ments, so as to be able to iterate over a scene to draw each element. to serve the purpose of reporting typing
We can obtain that ability by defining a new function that draw a list of information for objects. The elements
of scene are instances of various
display_elt objects:
classes (all consistent with class type
display_elt). OCaml selects the first
# let draw_list (d : display_elt list) : unit =
element of the list, which happens to be
# List.iter (fun x -> x#draw) d ;;
a border_rect instance, to serve as the
val draw_list : display_elt list -> unit = <fun> printable name of the type. This quirk
of OCaml reveals that the grafting of
We’ve put together a small scene (Figure 18.6), evocatively called the “O” part of the language isn’t always
seamless.
scene, to test the process.3
E X T E N S I O N A N D O B J E C T- O R I E N T E D P R O G R A M M I N G 347
let scene =
(* generate some graphical objects *)
let box = new border_rect {x = 100; y = 110} 180 210 in
let l1 = new rect {x = 70; y = 60} 20 80 in
let l2 = new rect {x = 135; y = 100} 20 160 in
let b = new circle {x = 100; y = 100} 40 in
let bu = new circle {x = 100; y = 140} 20 in
let h = new rect {x = 150; y = 170} 50 20 in
let t = new text {x = 100; y = 200} "The CS51 camel" in
(* bundle them together *)
let scene = [box; l1; l2; b; bu; h; t] in
(* change their color and translate them *)
List.iter (fun x -> x#set_color 0x994c00) scene;
List.iter (fun o -> let {x; y} = o#get_pos in
o#set_pos {x = x + 50; y = y + 40})
scene;
(* update the surround color *)
box#set_color G.blue;
scene ;;
# scene ;;
- : border_rect list = [<obj>; <obj>; <obj>; <obj>; <obj>; <obj>;
<obj>]
# test scene ;;
- : unit = ()
# end ;;
class type drawable = object method draw : unit end
The type of test shows that it now takes a drawable list argument.
We apply it to our scene.
# test scene ;;
Line 1, characters 5-10:
1 | test scene ;;
^^^^^
Error: This expression has type border_rect list
but an expression was expected of type drawable list
Type
border_rect =
< draw : unit; get_color : G.color; get_pos : point;
set_color : G.color -> unit; set_pos : point -> unit >
is not compatible with type drawable = < draw : unit >
The second object type has no method get_color
But the draw_list call no longer works. We’ve tripped over a limita-
tion in OCaml’s type inference. A subtype ought to be allowed where
a supertype is needed, as it is in the case of polymorphic subtypes of
less polymorphic supertypes. But in the case of class subtyping, OCaml
is not able to perform the necessary type inference to view the sub-
type as the supertype and use it accordingly. We have to give the type
inference system a hint.
We want the call to draw_list to view scene not as its display_elt
list subtype but rather as the drawable list supertype. We use the
:> operator to specify that view. The expression scene :> drawable
list specifies scene viewed as a drawable list.
# test scene ;;
- : unit = ()
Voila! The scene (Figure 18.7) appears. A little advice to the type infer-
ence mechanism has resolved the problem.
350 PROGRAMMING WELL
Here is a class type and class definition for “counter” objects. Each
object maintains an integer state that can be “bumped” by adding
an integer. The interface guarantees that only the two methods are
revealed.
Problem 209
Write a class definition for a class loud_counter obeying the same interface that works
identically, except that it also prints the resulting state of the counter each time the
counter is bumped.
Problem 210
Write a class type definition for an interface reset_counter_interface, which is
just like counter_interface except that it has an additional method of no arguments
intended to reset the state back to zero.
Problem 211
Write a class definition for a class loud_reset_counter satisfying the reset_counter_-
interface that implements a counter that both allows for resetting and is “loud”
(printing the state whenever a bump or reset occurs).
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
(a) (b)
18.7.1 Background
The unit vector that corresponds to the vector (6, 8) is the vector in
the same direction but whose norm is 1, which we can generate by just
dividing the vector by its norm: (0.6, 0.8). The distance between two
points is the norm of their difference, so the distance between (5, 5)
and (5.6, 5.8) is 1.
M A S S E S are like points, except that they have a physical mass and
forces can act upon them. Look at the file masses.mli for information
about the mass class, a subclass of point. Again, the interface file
documents the functionality of objects in the class, and we’ve provided
a partial implementation of the class.
C O N T R O L S are force-generating objects – like springs or alignment
constraints. We’ve provided the code for those in controls.ml. There
is a control class along with subclasses for different types of controls:
springs, alignment constraints, repulsive forces, and the like. Note
how the controls affect masses, so control objects typically have one
354 PROGRAMMING WELL
drawable
control
mass
Legend
• testXXX.ml: Files that provide tests that you can run to see the
system in action. You may want to add your own tests.
3. You should already be able to test the system on examples that only
use the circle graphical objects that we’ve provided. For instance,
the example in testuniformcentered.ml should already work.
Try building and running it and verify that the graphics window
launches and you see the animated layout process.
This section provides substantive background for one of the course prob-
lem sets. For common background on logistics and procedures for prob-
lem sets, including such topics as accessing the problem set code, compil-
ing and testing your code, reflecting on the process, and submitting your
solution, please review the course document “Problem set procedures”.
• Susceptible – The person has not been infected or has been infected
but is no longer immune.
• Recovered – The person was infected but recovered and has immu-
nity from further infection for a period of time.
We’ve provided you the basics of such a simulator, which you will
augment to be able to experiment with a wide range of scenarios.
The simulation can be visualized by showing the locations of the
people, color-coded as to their status. Figure 18.12 shows a visualiza-
tion of a simulation after some simulated time has elapsed. Suscep-
tibles are shown as small blue circles, infecteds as red, recovereds as
gray, and deceaseds as light gray “x” shapes. The radius around in-
fecteds where they can infect others is marked with a thin red circle as
well.
The visualization also provides a summary chart that shows the
proportion of the population in the different statuses over time. Fig-
ure 18.12 shows the full visualization containing the map and sum-
mary chart, and Figure 18.13 shows the visualization at the conclusion
of the simulated run. In this particular scenario, by the end of the run,
the infection had been eradicated.
The simulator is made up of several files, most of which you will not
need to modify. Those that you will be augmenting are given in italics.
counter config
Problem 212
By way of a warm-up exercise, you should implement the counter class in counter.ml.
This simple class is used to build some counters for maintaining statistics of how
many people are in each of the possible statuses and for tracking the time-steps in
the simulation. You can see how these counters are established in statistics.ml.
and then
% ./run.byte
to see the simulation in action. However, since only the two statuses of
susceptible and infected are implemented, and the latter only partially,
the simulation will not be complete and its visualization won’t conform
to the desired one.
Problem 213
Complete the implementation of the draw method for the infected class so that
infecteds show up as red circles with a radius marker as in Figure 18.12.
You now have a full simulation of the infection process, with people
cycling through from susceptible to infected to recovered to suscepti-
ble again. Use the opportunity to start experimenting with the various
configuration parameters in config.ml. What happens if you decrease
the neighbor radius (cNEIGHBOR_RADIUS), which is akin to “social
distancing”? What happens if you increase the step size (cSTEP_-
SIZE_SUSCEPTIBLE), which might be thought of as modeling increased
traveling. What happens if you greatly increase the immunity period?
Try out different scenarios and see what happens.
Finally, and perhaps most dramatically, the infection might have an
additional outcome, by virtue of its mortality.
Problem 216
Add a deceased class. In the infected class update method, after the infection period
is over, a proportion of people (governed by cMORTALITY) will become deceased rather
than recovered.
18.8.4 Exploration
Once you’ve got this all working, try out different scenarios. See how
parameters affect the results.
Feel free to augment the implementation. Here are some possibili-
ties, but you can certainly come up with your own.
• You might establish a capacity for treating infecteds such that when
there are more infecteds than the capacity, mortality increases. It
E X T E N S I O N A N D O B J E C T- O R I E N T E D P R O G R A M M I N G 361
would then become more important to “flatten the curve”. Can you
adjust parameters to do so? If so, which parameters work best?
Problem 217
To present your experiments, we ask that you make a short video of perhaps three to five
minutes presenting some scenarios that you’ve looked at. If they involve extensions of
the sort above, all the better.
To make the recording, you can use whatever video or screen-recording system you
prefer, but a simple one-person Zoom session using Zoom’s built-in local recording
feature should be sufficient.
19
Semantics: The environment model
The semantics for this language was provided through the apparatus of
evaluation rules, which defined derivations for judgements of the form
P ⇓v
E ⊢n⇓n (R int )
E ⊢P + Q ⇓
E ⊢P ⇓m
∣ (R + )
E ⊢Q ⇓n
⇓ m +n
Glossing again, the rule says “to evaluate an expression of the form P
+ Q in an environment E , first evaluate P in the environment E to an
integer value m and Q in the environment E to an integer value n. The
value of the full expression is then the integer literal representing the
sum of m and n.”
To construct a derivation for a whole expression using these rules,
we start in the empty environment {}. For instance, a derivation for
the expression 3 + 5 would be
{} ⊢ 3 + 5 ⇓
{} ⊢ 3 ⇓ 3
∣
{} ⊢ 5 ⇓ 5
⇓8
let x = D in B ⇓
D ⇓ vD
∣ (R let )
B [x ↦ v D ] ⇓ v B
⇓ vB
366 PROGRAMMING WELL
E ⊢ let x = D in B ⇓
E ⊢ D ⇓ vD
∣ (R let )
E {x ↦ v D } ⊢ B ⇓ v B
⇓ vB
According to this rule, “to evaluate an expression of the form let x
= D in B in an environment E , first evaluate D in E resulting in a
value v D and then evaluate the body B in an environment that is like E
except that the variable x is mapped to the value v D . The result of this
latter evaluation, v B , is the value of the let expression as a whole.”
In the substitution semantics, we will have substituted away all of
the bound variables in a closed expression, so no rule is needed for
evaluating variables themselves. But in the environment semantics,
since no substitution occurs, we’ll need to be able to evaluate expres-
sions that are just variables. Presumably, those variables will have
values in the prevailing environment; we’ll just look them up.
E ⊢ x ⇓ E (x ) (R var )
Exercise 218
Construct the derivation for the expression
let x = 3 in
let y = 5 in
x + y ;;
Exercise 219
Construct the derivation for the expression
let x = 3 in
let x = 5 in
x + x ;;
and the application of a function to its argument again adds the ar-
gument’s value to the environment used in evaluating the body of the
function:
E ⊢P Q ⇓
RRR E ⊢ P ⇓ fun x -> B
RRR
RRR E ⊢ Q ⇓ v (R app )
RRR Q
RRR E {x ↦ v } ⊢ B ⇓ v
R Q B
⇓ vB
Exercise 220
Provide glosses for these two rules.
We can try the example from Section 13.6:
(fun x -> x + x) (3 * 4)
{} ⊢ (fun x -> x + x) (3 * 4)
⇓
RRR {} ⊢ (fun x -> x + x) ⇓ (fun x -> x + x)
RRR
RRR {} ⊢ 3
RRR * 4⇓
RRR {} ⊢ 3 ⇓ 3
RRR ∣
RRR {} ⊢ 4 ⇓ 4
RRR
RRR ⇓ 12
RRR
RRR {x ↦ 12} ⊢ x + x ⇓
RRR
RRR {x ↦ 12} ⊢ x ⇓ 12
RRR ∣
RRR {x ↦ 12} ⊢ x ⇓ 12
RRR
RRR ⇓ 24
⇓ 24
368 PROGRAMMING WELL
E ⊢ x ⇓ E (x ) (R var )
E ⊢P + Q ⇓
E ⊢P ⇓m
∣ (R + )
E ⊢Q ⇓n
⇓ m +n
E ⊢ let x = D in B ⇓
E ⊢ D ⇓ vD
∣ (R let )
E {x ↦ v D } ⊢ B ⇓ v B
⇓ vB
E ⊢P Q ⇓
RRR E ⊢ P ⇓ fun x -> B
RRR
RRR E ⊢ Q ⇓ v (R app )
RRR Q
RRR E {x ↦ v } ⊢ B ⇓ v
R Q B
⇓ vB
# let x = 1 in
# let f = fun y -> x + y in
# let x = 2 in
# f 3 ;;
Line 3, characters 4-5:
3 | let x = 2 in
^
Warning 26: unused variable x.
- : int = 4
⇓
RRR
RRR 1 ⇓ 1
RRR let f = fun y -> 1 + y in let x = 2 in f 3
RRR
RRR ⇓
RRR
RRR RRR
RRR RRR fun y -> 1 + y ⇓ fun y -> 1 + y
RRR RRR let x = 2 in (fun y -> 1 + y) 3
RRR RRR
RRR RRR ⇓
RRR RRR
RRR R
RRR RRR
RRR RRR RRR 2 ⇓ 2
RRR RRR RRR (fun y -> 1 + y) 3
RRR RRR RRR
RRR RRR RRR ⇓
RRR RRR RRR
RRR R
RRR RRR fun y -> 1 + y ⇓ fun y -> 1 + y
RRR RRR
RRR RRR RRR RRR 3 ⇓ 3
RRR RRR RRR RRR
RRR RRR R
RRR RRR 1 + 3 ⇓
RRR RRR RRR RRR
RRR RRR RRR RRR
RRR RRR RRR 1⇓1
RRR RRR ∣
RRR RRR R
R ⇓3
RRR RRR RRR RRR 3
RRR RRR RRR RRR ⇓4
RRR RRR RRR R
RRR RRR RRR ⇓4
RRR RRR R
RRR RRR ⇓ 4
RRR R
RRR ⇓ 4
R
⇓4
Exercise 221
Before proceeding, see if you can construct the derivation for this expression according
to the environment semantics rules. Do you see where the difference lies?
According to the environment semantics developed so far, a deriva-
370 PROGRAMMING WELL
⇓5
The crucial difference comes when augmenting the environment
during application of the function fun y -> x + y to its argument.
Examine closely the two highlighted environments in the derivation
above. The first is the environment in force when the function is de-
fined, the L E X I C A L E N V I R O N M E N T of the function. The second is the
environment in force when the function is applied, its DY N A M I C E N -
V I R O N M E N T. The environment semantics presented so far augments
the dynamic environment with the new binding induced by the appli-
cation. It manifests a DY N A M I C E N V I R O N M E N T S E M A N T I C S . But for
consistency with the substitution semantics (which substitutes occur-
rences of a bound variable when the binding construct is defined, not
applied), we should use the lexical environment, thereby manifesting a
L E X I C A L E N V I R O N M E N T S E M A N T I C S.
In Section 19.2.2, We’ll develop a lexical environment semantics
that cleaves more faithfully to the lexical scope of the substitution
semantics, but first, we note some other divergences between dynamic
and lexical semantics.
Consider this simple application of a curried function:
SEMANTICS: THE ENVIRONMENT MODEL 371
⇓ ???
Notice how the body of the function, with its free occurrence of the
variable f, is evaluated in an environment in which f is bound to the
function itself. By using the dynamic environment semantics rules, we
get recursion “for free”. Consequently, the dynamic semantics rule for
the let rec construction can simply mimic the let construction:
E ⊢ let rec x = D in B ⇓
E ⊢ D ⇓ vD
∣ (R letrec )
E {x ↦ v D } ⊢ B ⇓ v B
⇓ vB
We’ll notate the closure that packages together a function P and its
environment E as [E ⊢ P ]. In evaluating a function, then, we merely
construct such a closure, capturing the function’s defining environ-
ment.
Ed ⊢ P Q ⇓
RRR E ⊢ P ⇓ [E ⊢ fun x -> B ]
RRR d l
RRR E ⊢ Q ⇓ v (R app )
RRR d Q
RRR E {x ↦ v } ⊢ B ⇓ v
R l Q B
⇓ vB
Exercise 222
Carry out the derivation using the lexical environment semantics for the expression
let x = 1 in
let f = fun y -> x + y in
let x = 2 in
f 3 ;;
Exercise 223
Carry out the derivation using the lexical environment semantics for the expression
(fun x -> fun y -> x + y) 1 2 ;;
Problem 224
In problem 170, you evaluated several expressions as OCaml would, with lexical scoping.
Which of those expressions would evaluate to a different value using dynamic scoping?
Exercise 225
Adjust the substitution semantics rules for booleans from Exercise 147 to construct
environment semantics rules for the constructs.
Exercise 226
Adjust the substitution semantics rules for conditional expressions (if ⟨⟩ then ⟨⟩ else ⟨⟩
) from Exercise 148 to construct environment semantics rules for the construct.
19.4 Recursion
It’s ill-formed because the lack of a rec keyword means that the f in
the definition part ought to be unbound. But it works just fine in the
dynamic environment semantics. When f 1 is evaluated in the dy-
namic environment in which f is bound to fun x -> if x = 0 then
1 else f (x - 1), all of the subexpressions of the definiens, includ-
ing the occurrence of f itself, will be evaluated in an augmentation
of that environment, so the “recursive” occurrence of f will obtain a
value. (It is perhaps for this reason that the earliest implementations of
functional programming languages, the original versions of LISP, used
a dynamic semantics.)
The lexical semantics, of course, does not benefit from this fortu-
itous accident of definition. The lexical environment in force when f
is defined is empty, and thus, when the body of f is evaluated, it is the
empty environment that is augmented with the argument x bound to
1. There is no binding for the recursively invoked f, and the deriva-
tion cannot be completed – consistent, by the way, with how OCaml
behaves:
# let rec x = x + 1 in x ;;
Line 1, characters 12-17:
1 | let rec x = x + 1 in x ;;
^^^^^
Error: This kind of expression is not allowed as right-hand side of
`let rec'
E ⊢ x ⇓ E (x ) (R var )
E ⊢P + Q ⇓
E ⊢P ⇓m
∣ (R + )
E ⊢Q ⇓n
⇓ m +n
E ⊢ let x = D in B ⇓
E ⊢ D ⇓ vD
∣ (R let )
E {x ↦ v D } ⊢ B ⇓ v B
⇓ vB
Ed ⊢ P Q ⇓
RRR E ⊢ P ⇓ [E ⊢ fun x -> B ]
RRR d l
RRR E ⊢ Q ⇓ v (R app )
RRR d Q
RRR E {x ↦ v } ⊢ B ⇓ v
R l Q B
⇓ vB
and the value type can include expression values and closures in a
simple variant type
type value =
| Val of expr
| Closure of (expr * env)
Exercise 227
E , S ⊢ P := Q ⇓
E , S ⊢ P ⇓ l , S′
∣ (R assign )
E , S ′ ⊢ Q ⇓ vQ , S ′′
⇓ (), S ′′ {l ↦ vQ }
The important point of the rule is the update to the store. But like
all evaluation rules, a value must be returned for the expression as a
whole. Here, we’ve simply returned the unit value ().
Exercise 228
In the presence of side effects, sequencing (with ;) becomes important. Write an evalua-
tion rule for sequencing.
To complete the semantics of mutable state, the remaining rules
must be modified to use and update stores appropriately. Figure 19.4
provides a full set of rules.
As an example of the deployment of these semantic rules, we con-
sider the expression
let x = ref 3 in
x := 42;
!x
E,S ⊢ P + Q ⇓
E , S ⊢ P ⇓ m, S ′
∣ (R + )
E , S ′ ⊢ Q ⇓ n, S ′′
⇓ m + n, S ′′
E , S ⊢ let x = D in B ⇓
E , S ⊢ D ⇓ vD , S′
∣ (R let )
E {x ↦ v D }, S ′ ⊢ B ⇓ v B , S ′′
⇓ v B , S ′′
Ed , S ⊢ P Q ⇓
RRR E , S ⊢ P ⇓ [E ⊢ fun x -> B ], S ′
RRR d l
RRR E , S ′ ⊢ Q ⇓ v , S ′′ (R app )
RRR d Q
RRR E {x ↦ v }, S ′′ ⊢ B ⇓ v , S ′′′
R l Q B
⇓ v B , S ′′′
SEMANTICS: THE ENVIRONMENT MODEL 381
⇓ l , S {l ↦ v P }
′
(where l is a new location)
E,S ⊢ ! P ⇓
∣ E , S ⊢ P ⇓ l , S′ (R deref )
⇓ S ′ (l ), S ′
E , S ⊢ P := Q ⇓
E , S ⊢ P ⇓ l , S′
∣ (R assign )
E , S ′ ⊢ Q ⇓ vQ , S ′′
⇓ (), S ′′ {l ↦ vQ }
E,S ⊢ P ; Q ⇓
E , S ⊢ P ⇓ (), S ′
∣ (R seq )
E , S ′ ⊢ Q ⇓ vQ , S ′′
⇓ vQ , S ′′
let rec x = D in B
as syntactic sugar for an expression that caches the recursion out using
just the trick described in Section 19.4: first assigning to x a mutable
reference to a special unassigned value, then evaluating the definition
D, replacing the value of x with the evaluated D, and finally, evaluating
B in that environment. We can carry out that recipe with the following
expression, which we can think of as the desugared let rec:
let x = ref unassigned in
x := D [x ↦ !x ];
B [x ↦ !x ]
E , S ⊢ let rec x = D in B ⇓
E {x ↦ l }, S {l ↦ unassigned} ⊢ D [x ↦ !x ] ⇓ v D , S ′
∣
E {x ↦ l }, S ′ {l ↦ v D } ⊢ B [x ↦ !x ] ⇓ v B , S ′′
⇓ v B , S ′′
(R letrec )
Problem 229
For the formally inclined, prove that the semantic rule for let rec above is equivalent to
the syntactic sugar approach.
20
Concurrency
20.2 Dependencies
20.3 Threads
resultA + resultB ;;
We can think of the two tasks (along with the computation of their
sum) as being executed in a single thread of computation. The se-
mantics of the let construct ensures that taskA () will be evaluated,
generating resultA, before taskB () begins its evaluation.
In order to demonstrate the idea, and prepare for the significantly
more subtle examples to come, we define a test function that takes two
functions as its argument, which play the roles of tasks A and B.
# let test_sequential taskA taskB =
# let resultA = taskA () in
# let resultB = taskB () in
# resultA + resultB ;;
val test_sequential : (unit -> int) -> (unit -> int) -> int = <fun>
The test returns the summed results 3. Along the way, various key
events are logged. We see the start of the short task and its ending,
followed by the start and end of the long task, indicating their sequen-
tiality.
If we’d like them to execute concurrently, we can establish a separate
thread (that is, a separate virtual processor) corresponding to taskA.
We refer to this process as F O R K I N G a new thread. We use OCaml’s
Thread.create function,3 which takes a function and its argument 3
The Thread module is part of OCaml’s
threads library. To make use of it in the
and returns a separate new thread of computation (a value of type
R E P L , you’ll need to make it available
Thread.t) in which the function is applied to its argument. Its type with
is thus (’a -> ’b) -> ’a -> Thread.t. So we can evaluate tasks A #use "topfind" ;;
#thread ;;
and B in separate threads, concurrently, as follows:
let threadA = Thread.create taskA () in
let resultB = taskB () in
...
...
let shared_result = ref None in
...
Now we can create a new thread for executing task A, saving its return
value in the shared result.
...
let _thread = Thread.create
(fun () -> shared_result := Some (taskA ())) () in
...
...
let resultB = taskB () in
...
Finally, we can extract the result from task A from the shared value, and
compute with the two results, by adding them as before.
...
match !shared_result with
| Some resultA ->
(* compute with result1 and result2 *)
resultA + resultB
| None ->
(* Oops, taskA hasn't completed! *)
failwith "shouldn't happen!" ;;
390 PROGRAMMING WELL
Again, we can test using the simulated tasks. To start, we fork the
new thread running the shorter task, with the longer task in the main
thread.
# test_communication task_short task_long ;;
[2.0848 task_long : starts]
[2.0849 task_short: starts]
[2.1850 task_short: ends]
[2.2850 task_long : ends]
- : int = 3
In this version of the test, the short task in the main thread completes
before the forked thread has time to complete the long task and update
the shared variable, leading to a run-time exception. The code has a
race condition with respect to a read-after-write dependency. These
two executions of the test demonstrate that, depending on which task
“wins the race”, the value to be read may or may not be written in time
as it needs to be.
CONCURRENCY 391
But this kind of brute force trick of repeatedly polling the shared vari-
able until it is ready is profligate and inelegant. It can waste compu-
tation that would be better allocated to other threads, and can waste
time if the delay is longer than needed.
392 PROGRAMMING WELL
Using this version of the test, the race condition is avoided, and the
calculation completes properly.
# test_communication task_long task_short ;;
[4.4136 task_short: starts]
[4.4137 task_long : starts]
[4.5139 task_short: ends]
[4.6139 task_long : ends]
- : int = 3
20.5 Futures
Exercise 230
Exercise 104 concerned implementing a fold operation over binary trees defined by
# type 'a bintree =
# | Empty
# | Node of 'a * 'a bintree * 'a bintree ;;
type 'a bintree = Empty | Node of 'a * 'a bintree * 'a bintree
Define a version of the fold operation, foldbt_conc, that performs the recursive folds of
the left and right subtrees concurrently, making use of futures to ensure that results are
available when needed.
Exercise 231
In
2. In the case of threads that are not intended to terminate, the whole
notion of a return value is inapplicable. Importantly, not all con-
current computations are intended to terminate. Indeed, one of the
benefits of concurrency as a programming concept is that it allows
multiple threads of nonterminating computation to interact. We
still need to manage the interaction so that the concurrent com-
putations satisfy the various dependencies among them without
dangerous race conditions.
CONCURRENCY 395
The deposit and withdraw methods both potentially affect the value
of the mutable balance variable. The withdraw function, in particular,
verifies that the balance is sufficient to cover the withdrawal amount,
updates the balance accordingly, and returns the amount to be dis-
pensed (0 if the balance is insufficient).
Now what happens when we try multiple concurrent withdrawals
from the same account? To simulate such an occurrence, the following
test_wds function carries out withdrawals of $75 and $50 in separate
threads (call them “thread A” and “thread B” for ease of reference) from
a single account with initial balance of $100, using a future for the
larger withdrawal. To track what goes on, the test function returns
the amount dispensed in thread A and thread B, along with the final
balance in the account.
396 PROGRAMMING WELL
# let test_wds () =
# let acct = new account 100 in
# let threadA_ftr = Future.future acct#withdraw 75 in
# let threadB = acct#withdraw 50 in
# let threadA = (Future.force threadA_ftr) in
# threadA, threadB, acct#balance ;;
val test_wds : unit -> int * int * int = <fun>
What behavior would we like to see in this case? One or the other
of the two withdrawals, whichever comes first, should see a sufficient
balance, dispense the requested amount, and update the balance
accordingly. The other attempted withdrawal should see a reduced and
insufficient balance and dispense no funds. Let’s try it.
# test_wds () ;;
- : int * int * int = (0, 50, 50)
Hmm.
In order to experiment with the possibility of interleavings of the
various components of the withdrawals, we make two changes to the
withdrawal simulation. First, we divide the balance update (balance
<- balance - amt)into two parts: the computation of the updated
balance and the update of the balance variable itself (let diff =
balance - amt in balance <- diff). Doing so separates the read-
ing of the shared balance from its writing, allowing interposition of
other threads in between. Then, we introduce some random delays
at various points in the computation: before the withdrawal first exe-
cutes, immediately after the balance check, and after computing the
updated balance just before carrying out the update. For this pur-
pose, we use a function random_delay, which pauses a thread for a
randomly selected time interval.
# let random_delay (max_delay : float) : unit =
# Thread.delay (Random.float max_delay) ;; valid? first second balance count
val random_delay : float -> unit = <fun> 75 50 −25 29
75 50 50 26
Updating the withdrawal function to insert these delays, we have 75 50 25 21
✓ 0 50 50 12
method withdraw (amt : int) : int =
✓ 75 0 25 12
random_delay 0.004;
if balance >= amt then begin Figure 20.5: Table of outcomes from
random_delay 0.001; multiple runs of simultaneous with-
let diff = balance - amt in drawals. Each row represents a possible
random_delay 0.001; outcome, with columns showing the
balance <- diff; amount dispensed for the first with-
drawal, the amount dispensed for the
amt
second withdrawal, the final balance,
end else 0 ;;
and the number of times this outcome
occurred in 100 trials. Only the check-
Here is a typical outcome from this simulation. marked trials are valid in respecting
dependencies.
# test_wds () ;;
- : int * int * int = (75, 50, -25)
CONCURRENCY 397
thread A ($75 withdrawal) thread B ($50 withdrawal) Figure 20.6: An unproblematic (essen-
tially sequential) interleaving of the
1. if balance >= amt then begin
2. let diff = balance - amt in threads.
3. balance <- diff;
4. amt
5. ⋯ if balance >= amt then begin
⋯
6. end else 0
If we run the simulation many times, we see (Figure 20.5) that the
result is quite variable. Certainly, there are many occurrences (about
half) showing the desired behavior, with either $75 or $50 dispensed
and a final balance of $25 or $50, respectively. But we also see plenty
of instances where both withdrawals go through, dispensing both $75
and $50, leaving a final balance of $−25. Or $25. Or $50. The use of
future ensures that the return value dependency is properly obeyed,
but the various dependencies having to do with the updates to and
uses of the account’s balance are uncontrolled. Different interleavings
of these operations can yield different results. Let’s examine a few of
the many possible interleavings.
First, thread A (the $75 withdrawal) may execute fully before thread
B (the $50 withdrawal) begins. That is, they may execute sequentially.
This interleaving is depicted in Figure 20.6. In this representation of
the two threads executing, the executed lines of thread A are on the
left, thread B on the right. We assume that each line of code executes
atomically, with the order of the numbered lines indicating the order
in which they are executed in the concurrent computation. The el-
lipses (⋯) indicate code lines that were not executed since they fell in
the non-chosen branch of a conditional. In line 1, the balance test in
thread A is evaluated. Since the balance is initially 100, and the with-
drawal amount is 75, the condition holds and lines 2-4 in the then
branch are executed. Line 3 in particular updates the shared balance
to 25, so that in line 5 when thread B tests the balance, the test fails
and the second withdrawal does not complete (line 6). In summary,
the $75 withdrawal attempt succeeds, dispensing the $75, and the $50
withdrawal attempt fails, leaving a balance of $25.
Of course, if thread B had executed fully before thread A, the cor-
responding result would have occurred, dispensing only the $50 and
leaving a balance of $50.
But other results are also possible. For instance, consider the in-
terleaving in Figure 20.7. Each thread verifies the balance as being
adequate and computes its updated value before the other performs
the balance update. Both threads go on to update the balance (lines
5 and 7); since thread B updates the balance later, its balance value,
$50, overwrites thread A’s $25 balance, so the final balance is $50. In
398 PROGRAMMING WELL
thread A ($75 withdrawal) thread B ($50 withdrawal) Figure 20.7: A problematic interleaving
of the threads.
1. if balance >= amt then begin
2. let diff = balance - amt in
3. if balance >= amt then begin
4. let diff = balance - amt in
5. balance <- diff;
6. amt
7. ⋯ balance <- diff;
8. amt
⋯
Exercise 232
Construct an interleaving in which both withdrawals succeed, leaving a balance of $25.
Exercise 233
Construct an interleaving in which both withdrawals succeed, leaving a balance of $ − 25.
As Figure 20.5 shows, and these possible interleavings explain,
there are important dependencies that are not being respected in the
concurrent implementation of the account operations. A solution to
this problem of controlling data dependencies requires further tools.
20.7 Locks
4
As shown, this creates a lock of type Mutex.t. We can then lock Crucially, the testing for unlocked
status and subsequent locking occur
and unlock the lock as needed with the functions Mutex.lock and atomically, so that other threads can’t
Mutex.unlock. interleave between them. How this is
accomplished, the subject of fundamen-
The mutex locks work as follows. When Mutex.lock is called on a
tal research in concurrent computation,
lock, the lock is first verified to be in its unlocked state. If so, the lock is well beyond the scope of this text.
switches to the locked state and computation proceeds.4 But if not,
CONCURRENCY 399
thread A ($75 withdrawal) thread B ($50 withdrawal) Figure 20.8: The problematic interleav-
ing, corrected by the use of locks.
1. Mutex.lock balance_lock;
2. if balance >= amt then begin
3. let diff = balance - amt in
Mutex.lock balance_lock;
4. balance <- diff;
5. amt
ÔÔ⇒
⋯ thread B suspended
6. Mutex.unlock balance_lock;
7. Mutex.lock balance_lock;
8. if balance >= amt then begin
9. let diff = balance - amt in
10. balance <- diff;
11. amt
⋯
12. Mutex.unlock balance_lock;
the thread in which the call was made is suspended until such time as
the lock becomes unlocked, presumably by a call to Mutex.unlock in
another thread.
Inserting the locks in the ATM example, we would have a withdraw
method like this:
method withdraw (amt : int) : int =
Mutex.lock balance_lock;
if balance >= amt then begin
balance <- balance - amt;
amt
end else 0;
Mutex.unlock balance_lock ;;
This idiom – wrapping a critical region with a lock at the beginning and
an unlock at the end – captures the stereotypical use of locks.
In this idiom, the lock is explicitly unlocked after the need for the
lock is over. The unlocking is crucial; without it, other threads would
400 PROGRAMMING WELL
Exercise 234
Define a version of with_lock that handles exceptions by making sure to unlock the
lock.
Using with_lock, the withdraw method becomes
valid results, as depicted in Figure 20.9. Figure 20.9: Rerunning the test of
simultaneous withdrawals, with locking
in place, all trials now respect the
20.8 Deadlock dependencies, though the results can
still vary depending on which of the two
withdrawals in each trial happens to
occur first.
21
Final project: Implementing MiniML
21.1 Overview
As with all the individual problem sets in the course, your project is to
be done individually, under the course’s standard rules of collabora-
tion. (The sole exception is described in Section 21.6.) You should not
share code with others, nor should you post public questions about
your code on Piazza. If you have clarificatory questions about the
project assignment, you can post those on Piazza and if appropriate
we will answer them publicly so the full class can benefit from the
clarification.
The final project will be graded based on correctness of the imple-
mentation of the first two stages; design and style of the submitted
code; and scope of the project as a whole (including the extensions) as
demonstrated by a short paper describing your extensions, which is
assessed for both content and presentation.
It may be that you are unable to complete all the code stages of the
final project. You should make sure to keep versions at appropriate
milestones so that you can always roll back to a working partial project
to submit. Using git will be especially important for this version
tracking if used properly.
Some students or groups might prefer to do a different final project
on a topic of their own devising. For students who have been doing
exceptionally well in the course to date, this may be possible. See
Section 21.6 for further information.
type unop =
| Negate ;;
type binop =
| Plus
| Minus
| Times
| Equals
| LessThan ;;
type expr =
| Var of varid (* variables *)
| Num of int (* integers *)
| Bool of bool (* booleans *)
| Unop of unop * expr (* unary operators *)
| Binop of binop * expr * expr (* binary operators *)
| Conditional of expr * expr * expr (* if then else *)
| Fun of varid * expr (* function def'ns *)
| Let of varid * expr * expr (* local naming *)
| Letrec of varid * expr * expr (* rec. local naming *)
| Raise (* exceptions *)
| Unassigned (* (temp) unassigned *)
| App of expr * expr ;; (* function app'ns *)
Exercise 235
Write a function exp_to_concrete_string : expr -> string that converts an
abstract syntax tree of type expr to a concrete syntax string. The particularities of what
concrete syntax you use is not crucial so long as you do something sensible along the
lines we’ve exemplified. (This function will actually be quite helpful in later stages.)
To get things started, we also provide a parser for the MiniML lan-
guage, which takes a string in a concrete syntax and returns a value of
this type expr; you may want to extend the parser in a later part of the
project (Section 21.4.3).1 The compiled parser and a read-eval-print 1
The parser that we provide makes use
of the OCaml package menhir, which
loop for the language are available in the following files:
is a parser generator for OCaml. You
should have installed it as per the setup
evaluation.ml The future home of anything needed to evaluate ex- instructions provided at the start of the
pressions to values. Currently, it provides a trivial “evaluator” course by running the following opam
command:
eval_t that merely returns the expression unchanged.
% opam install -y menhir
miniml.ml Runs a read-eval-print loop for MiniML, using the The menhir parser generator will be
Evaluation module that you will complete. discussed further in Section 21.4.3.
F I N A L P R O J E C T: I M P L E M E N T I N G M I N I M L 405
Implement the function exp_to_abstract_string : expr -> You can safely ignore this message
from the parser generator, which is
string to convert abstract syntax trees to strings representing their reporting on some ambiguities in the
structure and test it thoroughly. If you did Exercise 235, the experience MiniML grammar that it has resolved
automatically.
may be helpful here, and you’ll want to also implement exp_to_-
concrete_string : expr -> string for use in later stages as well.
The particularities of what concrete syntax you use to depict the ab-
stract syntax is not crucial – we won’t be checking it – so long as you do
something sensible along the lines we’ve exemplified.
After this (and each) stage, it would be a good idea to commit the
changes and push to your remote repository as a checkpoint and
backup.
# ./miniml.byte
Entering miniml.byte...
<== 3 ;;
==> Num(3)
<== 3 4 ;;
==> App(Num(3), Num(4))
<== (((3) ;;
xx> parse error
406 PROGRAMMING WELL
Exercise 237
Familiarize yourself with how this “almost” R E P L works. How does eval_t get called?
What does eval_t do and why? What’s the point of the Env.Val in the definition? Why
does eval_t take an argument _env : Env.env, which it just ignores? (These last two
questions are answered a few paragraphs below. Feel free to read ahead.)
To actually get evaluation going, you’ll need to implement a substi-
tution semantics, which requires completing the functions in the Expr
module.
Stage 238
Start by writing the function free_vars in expr.ml, which takes an
expression (expr) and returns a representation of the free variables
in the expression, according to the definition in Figure 13.3. Test this
function completely.
Stage 239
Next, write the function subst that implements substitution as defined
in Figure 13.4. In some cases, you’ll need the ability to define new fresh
variables in the process of performing substitutions. You’ll see we call
for a function new_varname to play that role. Looking at the gensym
function that you wrote in lab might be useful for that. Once you’ve
written subst make sure to test it completely.
Stage 240
Implement the eval_s : Expr.expr -> Env.env -> Env.value
function in evaluation.ml. (You can hold off on completing the
implementation of the Env module for the time being. That comes into
play in later sections.) We recommend that you implement it in stages,
from the simplest bits of the language to the most complex. You’ll want
to test each stage thoroughly using unit tests as you complete it. Keep
these unit tests around so that you can easily unit test the later versions
of the evaluator that you’ll develop in future sections.
sion before printing the result. It’s more pleasant to read the output
expression in concrete rather than abstract syntax, so you can replace
the exp_to_abstract_string call with a call to exp_to_concrete_-
string. You should end up with behavior like this:
# miniml_soln.byte
Entering miniml_soln.byte...
<== 3 ;;
==> 3
<== 3 + 4 ;;
==> 7
<== 3 4 ;;
xx> evaluation error: (3 4) bad redex
<== (((3) ;;
xx> parse error
<== let f = fun x -> x in f f 3 ;;
==> 3
<== let rec f = fun x -> if x = 0 then 1 else x * f (x - 1) in f 4 ;;
xx> evaluation error: not yet implemented: let rec
<== Goodbye.
Stage 241
After you’ve changed evaluate to call eval_s, you’ll have a complete
working implementation of MiniML. As usual, you should save a snap-
shot of this using a git commit and push so that if you have trouble
down the line you can always roll back to this version to submit it.
let and let rec are awkward to implement, and extending the lan-
guage to handle references, mutability, and imperative programming
is impossible. For that, you’ll extend the language semantics to make
use of an environment that stores a mapping from variables to their
values, as described in Chapter 19. We’ve provided a type signature for
environments. It stipulates types for environments and values, and
functions to create an empty environment (which we’ve already imple-
mented for you), to extend an environment with a new B I N D I N G , that
is, a mapping of a variable to its (mutable) value, and to look up the
value associated with a variable.
The implementation of environments for the purpose of this project
follows that described in Section 19.5. We make use of an environment
that allows the values to be mutable:
Stage 242
Implement the various functions involved in the Env module and test
them thoroughly.
(fun x -> x + x) 5
E ⊢P Q ⇓
RRR E ⊢ P ⇓ fun x -> B
RRR
RRR E ⊢ Q ⇓ v (R app )
RRR Q
RRR E {x ↦ v } ⊢ B ⇓ v
R Q B
⇓ vB
E ⊢ let x = D in B ⇓
E ⊢ D ⇓ vD
∣ (R let )
E {x ↦ v D } ⊢ B ⇓ v B
⇓ vB
Stage 243
Implement another evaluation function eval_d : Expr.expr ->
Env.env -> Env.value (the ‘d’ is for dynamically scoped environment
semantics), which works along the lines just discussed. Make sure to
test it on a range of tests exercising all the parts of the language.
F I N A L P R O J E C T: I M P L E M E N T I N G M I N I M L 411
In this final part of the project, you will extend MiniML in one or more
ways of your choosing.
Here are a few ideas for extending the language, very roughly in or-
der from least to most ambitious. Especially difficult extensions are
marked with ¢ symbols.
1. Add additional atomic types (floats, strings, unit, etc.) and corre-
sponding literals and operators.
7. Add laziness to the language (by adding refs and syntactic sugar for
the lazy keyword). If you’ve also added lists, you’ll be able to build
infinite streams.
10. ¢¢ Add type inference to the language, so that (as in OCaml) types
are inferred even when not given explicitly. This is extremely ambi-
tious, not for the faint of heart. Do not attempt to do this.
412 PROGRAMMING WELL
Most of the extensions (in fact, all except for (2)) require extensions
to the concrete syntax of the language. We provide information about
extending the concrete syntax in Section 21.4.3. Many other extensions
are possible. Don’t feel beholden to this list. Be creative!
In the process of extending the language, you may find the need to
expand the definition of what an expression is, as codified in the file
expr.mli. Other modifications may be necessary as well. That is, of
course, expected, but you should make sure that you do so in a manner
compatible with the existing codebase so that unit tests based on the
provided definitions continue to function. The ability to submit your
code for testing should help with this process. In particular, if you have
to make changes to mli files, you’ll want to do so in a way that extends
the signature, rather than restricting it.
Most importantly: It is better to do a great job (clean, elegant de-
sign; beautiful style; well thought-out implementation; evocative
demonstrations of the extended language; literate writeup) on a
smaller extension, than a mediocre job on an ambitious extension.
That is, the scope aspect of the project will be weighted substantially
less than the design and style aspects. Caveat scriptor.
let x = 1 in
let f = fun y -> x + y in
let x = 2 in
f 3 ;;
Exercise 244
What should this expression evaluate to? Test it in the OCaml interpreter. Try this
expression using your eval_s and eval_d evaluators. Which ones accord with OCaml’s
evaluation?
The eval_d evaluator that you’ve implemented so far is dynamically
scoped. The values of variables are governed by the dynamic ordering
in which they are evaluated. But OCaml is lexically scoped. The values
of variables are governed by the lexical structure of the program. (See
Section 19.2.2 for further discussion.) In the case above, when the
function f is applied to 3, the most recent assignment to x is of the
value 2, but the assignment to the x that lexically outscopes f is of the
value 1. Thus a dynamically scoped language calculates the body of f,
x + y, as 2 + 3 (that is, 5) but a lexically scoped language calculates
the value as 1 + 3 (that is, 4).
F I N A L P R O J E C T: I M P L E M E N T I N G M I N I M L 413
Stage 245
(if you decide to do a lexically scoped evaluator in service of your ex-
tension) Make a copy of your eval_d evaluation function and call it
eval_l (the ‘l’ for lexically scoped environment semantics). Modify the
code so that the evaluation of a function returns a closure containing
the function itself and the current environment. Modify the function
application part so that it evaluates the body of the function in the
lexical environment from the corresponding closure (appropriately
updated). As usual, test it thoroughly. If you’ve carefully accumulated
good unit tests for the previous evaluators, you should be able to fully
test this new one with just a single function call.
Do not just modify eval_d to exhibit lexical scope, as this will cause
our unit tests for eval_d (which assume that it is dynamically scoped)
to fail. That’s why we ask you to define the lexically scoped evaluator
as eval_l. The copy-paste recommendation for building eval_l from
eval_d makes for simplicity in the process, but will undoubtedly leave
you with redundant code. Once you’ve got this all working, you may
want to think about merging the two implementations so that they
share as much code as possible. Various of the abstraction techniques
you’ve learned in the course could be useful here.
The let rec expression has three parts: a variable name, a definition
expression, and a body. To evaluate it, we ought to first evaluate the
definition part, but using what environment? If we use the incoming
(empty) environment, then what will we use for a value of f when we
reach it? Ideally, we should use the value of the definition, but we don’t
have it yet.
Following the approach described in Section 19.6.1, in the interim,
we’ll extend the environment with a special value, Unassigned, as the
value of the variable being recursively defined. You may have noticed
this special value in the expr type; uniquely, it is never generated by
the parser. We evaluate the definition in this extended environment,
hopefully generating a value. (The definition part better not ever eval-
uate the variable name though, as it is unassigned; doing so should
raise an EvalError. An example of this run-time error might be let
rec x = x in x.) The value returned for the definition can then re-
place the value for the variable name (thus the need for environments
to map variables to mutable values) and the environment can then be
used in evaluating the body.
In the example above, we augment the empty environment with a
binding for f to Unassigned and evaluate fun x -> if x = 0 then
x else f (x - 1) in that environment. Since this is a function, it
is already a value, so evaluates to itself. (Notice how we never had to
evaluate f in generating this value.)
Now the environment can be updated to have f have this function
as a value – not extended (using the extend function) but *actually
modified* by replacing the value stored in the value ref associated
with f in the environment. Finally, the body f 2 is evaluated in this
environment. The body, an application, evaluates f by looking it up in
this environment yielding fun x -> if x = 0 then x else f (x -
1) and evaluates 2 to itself, then evaluates the body of the function in
the prevailing environment (in which f has its value) augmented with a
binding of x to 2.
In summary, a let rec expression like let rec x = D in B is
evaluated via the following five-step process:
5. Return v_B.
F I N A L P R O J E C T: I M P L E M E N T I N G M I N I M L 415
Stage 246
Write up your extensions in a short but formal paper describing and
demonstrating any extensions and how you implemented them.
Use Markdown or LATEX format, and name the file writeup.md or
writeup.tex. You’ll submit both the source file and a rendered PDF
file.
the final project (such as your writeup or any code files for testing)
before submitting. You can run git status to see if there are any
untracked files in your repository. Finally, remember that you can look
on Gradescope to check that your submissions contains the files you
expect. Unfortunately, we can’t accept any files that are not submitted
on time.
Students who have been doing exceptionally well in the course to date
can petition to do alternative final projects of their own devising, under
the following stipulations:
3. You will want to talk to course staff about your ideas early to get
initial feedback.
4. You will need to submit a proposal for the project by April 16, 2021.
The proposal should describe what the project goals are, how you
will go about implementing the project, and how the work will be
distributed among the members of the group (if applicable).
7. You will submit the project results, including all code, a demon-
stration of the project system in action, and a paper describing the
project and any results, by May 5, 2021.
9. The group as a whole may drop out of the process at any time.
Individual members of the group would then submit instead the
standard final project described here.
A
Mathematical background and notations
A.1 Functions
fact (0) = 1
fact (n ) = n ⋅ fact (n − 1) for n > 0
1 for n = 0
fact (n ) = {
n ⋅ fact (n − 1) for n > 0
A.1.2 Notating
The equation function
p = 511 application
500 describes a correspondence between the number of
tickets sold and the profit. This correspondence is a function whose domain is
Inthe
thesetfactorial
of ticketsexample,
that couldwe
possibly
used bethesold.
familiar mathematical notation
domain D {O, I, 2, ... }.
for applying a function to an argument – naming the function followed
The range is the set of profits that are possible, including "negative profits,"
by its argument in parentheses: fact (n ).
or losses, if too few tickets are sold.
range R = {-500, -495, -490, ...}.
If we call this profit function p. we can use arrow notation and write Figure A.1: A snippet from a typical
middle school algebra textbook (Brown
the rule P: 11 -7 511 - 500,
et al., 2000, page 379), introducing
which is read "the function P that assigns 511 - 500 to II" or "the function P standard mathematical function
that pairs 11 with 511 - 500." We could also use functional notation: application notation.
P(I1) = 511 500
which is read "P of 11 equals 511 - 500" or "the value of P at 11 is 511 - 500."
To specify a function completely, you must describe the domain of the
function as well as give the rule. The numbers assigned by the rule then form
the range of the function.
Some time in your primary education, perhaps in middle school,
you were taught this standard mathematical notation for applying a
Example 1 List the range of
function to one or g: more
x -7 arguments.
4 + 3x - x 2 In Figure A.1, a snapshot from
x 4 + 3x -./
a middle schoolifalgebra textbook shows where
the domain D = {-I, 0, I, 2}. this notation is first
-I 4 + 3(-1) (-1)2 = 0
taught: “We could also use functional notation: P (n ) = 5n − 500, which 2
Solution In 4 + 3x - x 2 replace x with each 0 4 + 3(0) - 0 = 4
is read ‘P of n equals 5n − 500.’” In this notation, functions can take one2
member of D to find the members
I 4 + 3(1) - 1 = 6
or more arguments,
of the notated
range R. by placing the arguments in parentheses
2
:. R 6} Answerthe function name. This
= {O, 4, following
2 4 + 3(2) - 2 = 6
and separated by commas notation
is so familiar that it’s hard to imagine that someone had to invent it.
Note that did.
But someone In fact,gitinwas
the function Example I assigns
the 18th the number
century 6 to both I
Swiss mathematician
and 2. In listing the range of g, however, you name 6 only once.
Leonhard Eulerofwho
Members in 1734
the range of a first used
function arethis notation
called (Figure
values of A.3). Since
the function. In
then, it hasI, become
Example the valuesuniversal.
of the function g are
At this 0, 4, the
point, and notation
6. To indicate
is sothat the
familiar
function g assigns to 2 the value 6, you write
that it is impossible to see f (1, 2, 3) without immediately interpreting it
g(2) = 6,
as the application of the function f to arguments 1, 2, and 3.
which is read "g of 2 equals 6" or "the value of g at 2 is 6." Note that g(2)
isIt110t
is thus perhaps
the product of gsurprising that the
and 2. It names OCaml doesn’t
number that g use this
assigns to notation
2.
for function application. Instead, it follows the notational convention
Introduction to Functions 379
proposed by the Princeton mathematician and logician Alonzo Church
in his so-called lambda calculus (Section A.1.4), a logic of functions.
In the lambda calculus, functions and their application are so central
(indeed, there’s basically nothing else in the logic) that the addition of Figure A.2: Leonhard Euler (1707–1783)
the parentheses in the function application notation is too onerous. invented the familiar parenthesized
notation for function application.
Instead, Church proposed merely prefixing the function to its argu-
ment. Instead of f (1), Church’s notation would have f 1. Instead of
f (g (1)), f (g 1).
0! = 1
n ! = n ⋅ (n − 1)! for n > 0
420 PROGRAMMING WELL
The point is that the Euler notation is not the only one that can be or is
used for function application. Here are some more examples:
√ √
λn. n 2 The function from n to n 2 , that is, the absolute Table A.1: A few functions in lambda
notation, with their English glosses and
value function, or, in OCaml:
their approximate OCaml equivalents.
fun n -> sqrt(n *. n)
λn.(m ⋅ n ) 2
the function from n to m ⋅ n 2 , so that m is implicitly
being viewed as a constant:
fun n -> m *. (n *. n)
λm.(m ⋅ n 2 ) the function from m to m ⋅ n 2 , so that n is implicitly
being viewed as a constant:
fun m -> m *. (n *. n)
λm.λn.(m ⋅ n ) 2
the function from m to a function from n to m ⋅ n 2 :
fun m -> fun n -> m *. (n *. n)
under a different concrete syntax. The keyword fun plays the role of λ
and the operator -> plays the role of the period. In fact, the ability to
p q p and q p or q not p
define anonymous functions, so central to functional programming
true true true true false
languages, is inherited directly from the lambda notation that gives its
true false false true false
name to Church’s calculus. false true false true true
As shown in Table A.1, each of the examples above could be false false false false true
rephrased in OCaml. You may recognize the last of these as an example Figure A.5: The three boolean operators
defined.
of a curried function (Section 6.1).
When there’s a need for specifying mathematical functions directly,
unnamed, we will take advantage of Church’s lambda notation, espe-
cially in Chapter 14.
A.2 Logic
hypotenuse
B
The logic of propositions, boolean logic, underlies the bool type. In-
c
formally, propositions are conceptual objects that can be either true or a
false. Propositions can be combined or transformed with various oper- right angle
ations. The C O N J U N C T I O N of two propositions p and q is true just in C A
b
case both p and q are true, and false otherwise. The D I S J U N C T I O N is
true just in case either p or q (or both) are true. The N E G AT I O N of p is Figure A.6: A right triangle. Angle C is a
true just in case p is not true (that is, p is false). Conjunction, disjunc- right angle. The opposite side, of length
c, is the hypotenuse. By Pythagorus’s
tion, and negation thus correspond roughly to the English words “and”,
theorem, a 2 + b 2 = c 2 .
“or”, and “not”, respectively, and for that reason, we sometimes speak
of the “and” of two boolean values, or their “or”. (See Figure A.5.)
There are other operations on boolean values considered in logic –
for instance, the conditional, glossed by “if . . . then . . . ”; or the exclusive
“or” – but these three are sufficient for our purposes. For more back-
ground on propositional logic, see Chapter 9 of the text by Lewis and
Zax (2019).
(x2 , y2 )
(x2 x1 )2 + (y2 y1 )2
A.3 Geometry y2 y1
root.
The ratio of the circumference of a circle and its diameter is (non- c
trivially, and perhaps surprisingly) a constant, conventionally called π r
(read, “pi”), and approximately 3.1416. This constant is also the ratio o
A d
of the area of a circle to the area of a square whose side is the circle’s
radius. Thus, using the nomenclature of Figure A.8, c = πd = 2πr and
A = πr 2 . Figure A.8: Geometry of the circle at
origin o of radius r , diameter d = 2r ,
circumference c, and area A.
A.4 Sets
Union: s ∪ t is the U N I O N of sets s and t , that is, the set containing all
the elements that are in either of the two sets;
this notation:
x [x ↦ P ] = P
y [x ↦ P ] = y where x ≡
/y
Like all rules, those below are not to be followed slavishly. Rather, they
should be seen as instances of these underlying principles. These
principles may sometimes be in conflict, in which case judgement is
required in finding the best way to write the code. This is one of the
many ways in which programming is an art, not (just) a science.
This guide is not complete. For more recommendations, from the
OCaml developers themselves, see the official OCaml guidelines.
B.1 Formatting
You may feel inclined to use tab characters ( A S C I I 0x09) to align text.
Do not do so; use spaces instead. The width of a tab is not uniform
across all renderings, and what looks good on your machine may look
terrible on another’s, especially if you have mixed spaces and tabs.
Some text editors map the tab key to a sequence of spaces rather than
a tab character; in this case, it’s fine to use the tab key.
The obvious way to stay within the 80 character limit imposed by the
rule above is to press the enter key every once in a while. However,
blank lines should only be used at major logical breaks in a program,
for instance, between value declarations, especially between function
declarations. Often it is not necessary to have blank lines between
other declarations unless you are separating the different types of
declarations (such as modules, types, exceptions and values). Unless
function declarations within a let block are long, there should be no
blank lines within a let block. There should absolutely never be a
blank line within an expression.
7 if condition then
(do this;
do that;
do the other)
else
(do something else entirely;
do this too);
do in any case
Judgement can be applied to vary from these rules for clarity’s sake,
for instance, when emphasizing precedence.
It’s better to place breaks at operators higher in the abstract syntax tree,
to emphasize the structure.
In the case of delimiters, however, line breaks should occur after the
delimiter.
B.1.7 Indentation
3
if exp1 then veryshortexp2 else veryshortexp3
When the branches are too long for a single line, move the else onto its
own line.
3
if exp1 then exp2
else exp3
3
if exp1 then shortexp2
else if exp3 then shortexp4
else if exp5 then shortexp6
else exp8
For very long then or else branches, the branch expression can be
indented and use multiple lines.
3
if exp1 then
longexp2
else shortexp3
3
if exp1 then
longexp2
else
longexp3
Some use an alternative conditional layout, with the then and else
keywords starting their own lines.
7
if exp1
then exp2
else exp3
3 let x = definition in
code_that_uses_x
7 let x = definition in
code_that_uses_x
let x = x_definition in
let y = y_definition in
let z = z_definition in
block_that_uses_all_the_defined_notions
B.2 Documentation
The latter is the better style, although you may find some source code
that uses the first. Comments should be indented to the level of the
line of code that follows the comment.
A STYLE GUIDE 431
arguing that the aligned asterisks demarcate the comment well when
it is viewed without syntax highlighting. Others find this style heavy-
handed and hard to maintain without good code editor support (for
instance, emacs Tuareg mode doesn’t support it well), leading to this
alternative:
(* This is one of those rare but long comments
that need to span multiple lines because
the code is unusually complex and requires
extra explanation.
*)
let complicated_function () = ...
Table B.1 provides the naming convention rules that are followed by
OCaml libraries. You should follow them too. Some of these naming
conventions are enforced by the compiler; these are shown in boldface
below. For example, it is not possible to have the name of a variable
start with an uppercase letter.
432 PROGRAMMING WELL
Variable names should describe what the variables are for, in the form
of a word or sequence of words. Proper naming of a variable can be the
best form of documentation, obviating the need for any further doc-
umentation. By convention (Table B.1) the words in a variable name
are separated by underscores (multi_word_name), not (ironically)
distinguished by camel case (multiWordName).
(Of course, this function can be specified even more compactly as (<>)
None.)
Often it is the case that a function used in a fold, filter, or map is
named f. Here is an example with appropriate variable names:
A STYLE GUIDE 433
Take advantage of the fact that OCaml allows the prime character ’
in variable names. Use it to make clear related functions:
let reverse (lst : 'a list) =
let rec reverse' remaining accum =
match remaining with
| [] -> accum
| hd :: tl -> reverse' tl (hd :: accum) in
reverse' lst [] ;;
Not only is this more explanatory – we understand that the final mul-
tiplication is to account for taxes – it allows for a single point of code
change if the tax rate changes.
7 let succ x = x + 1
7 let rec zip3 (x : 'a list) (y : 'b list) (z : 'c list) : ('a * 'b *
'c) list option =
...
434 PROGRAMMING WELL
Mutable values, on the rare occasion that they are necessary at all,
should be local to functions and almost never declared as a structure’s
value. Making a mutable value global causes many problems. First,
an algorithm that mutates the value cannot be ensured that the value
is consistent with the algorithm, as it might be modified outside the
function or by a previous execution of the algorithm. Second, having
global mutable values makes it more likely that your code is nonreen-
trant. Without proper knowledge of the ramifications, declaring global
mutable values can easily lead not only to bad design but also to incor-
rect code.
You should rarely need to rename values: in fact, this is a sure way to
obfuscate code. Renaming a value should be backed up with a very
good reason. One instance where renaming a variable is both common
and reasonable is aliasing modules. In these cases, other modules used
by functions within the current module are aliased to one or two letter
variables at the top of the struct block. This serves two purposes: it
shortens the name of the module and it documents the modules you
use. Here is an example:
module H = Hashtbl
module L = List
module A = Array
...
When declaring elements in a file (or nested module) you first alias
the modules you intend to use, then declare the types, then define
exceptions, and finally list all the value declarations for the module.
A STYLE GUIDE 435
module L = List
type foo = int
exception InternalError
let first list = L.nth list 0
7 let f arg1 =
let x = arg1.foo in
let y = arg1.bar in
let baz = arg1.baz in
...
7 match e with
| true -> x
| false -> y
3 if e then x else y
and
7 match e with
| c -> x (* c is a constant value *)
| _ -> y
3 if e = c then x else y
A STYLE GUIDE 437
3 let x, _ = expr in
...
7 let v = some_function () in
let x = fst v in
let y = snd v in
x + y
3 let x, y = some_function () in
x + y
Don’t use List.hd or List.tl at all The functions hd and tl are used
to deconstruct list types; however, they raise exceptions on certain
arguments. You should never use these functions. In the case that you
find it absolutely necessary to use these (something that probably
won’t ever happen), you should explicitly handle any exceptions that
can be raised by these functions.
B.5 Verbosity
3 if e then x else y
7 3
if e then true else false e
if e then false else true not e
if e then e else false e
if x then true else y x || y
if x then y else false x && y
if x then false else y not x && y
3 if e then y else x
3
A STYLE GUIDE 439
You can even do this when the function is an infix binary operator,
though you’ll need to place the operator in parentheses.
3 List.fold_left (+) 0
When computing values more than once, you may be wasting CPU
time (a design consideration) and making your program less clear (a
style consideration) and harder to maintain (a consideration of both
design and style). The best way to avoid computing things twice is to
create a let expression and bind the computed value to a variable
name. This has the added benefit of letting you document the purpose
of the value with a well-chosen variable name, which means less com-
menting. On the other hand, not every computed sub-value needs to
be let-bound.
7 3
x :: [] [x]
length + 0 length
length * 1 length
big_expression * big_expression let x = big_expression in x * x
if x then f a b c1 else f a b c2 f a b (if x then c1 else c2)
String.compare x y = 0 x = y
String.compare x y < 0 x < y
String.compare y x < 0 x > y
C
Solutions to selected exercises
⟨nounphrase ⟩ ⟨noun ⟩
mad ⟨noun ⟩
tea
Solution to Exercise 4 There are three structures given the rules pro-
vided, corresponding to eaters of flying purple people, flying eaters of
purple people, and flying purple eaters of people.
Solution to Exercise 6
1. +
~- 6
2. ~-
4 6
442 PROGRAMMING WELL
3. +
/ 6
20 ~-
4. *
5 +
3 4
5. *
+ 5
4 3
6. *
+ 5
+ 0
3 4
1. ~- (1 + 42)
2. 84 / (0 + 42)
3. 84 + 0 / 42 or 84 + (0 / 42)
Note the consistent use of floating point literals and operators, without
which you’d get errors like this:
Solution to Exercise 9 The fourth and seventh might have struck you
as unusual.
Why does 3.1416 = 314.16 /. 100. turn out to be false? Float-
ing point arithmetic isn’t exact, so that the division 314.16 /. 100.
yields a value that is extremely close to, but not exactly, 3.1416, as
demonstrated here:
# 314.16 /. 100. ;;
- : float = 3.14160000000000039
Why is false less than true? It turns out that all values of a type are
ordered in this way. The decision to order false as less than true was
arbitrary. Universalizing orderings of values within a type allows for the
ordering operators to be polymorphic, which is quite useful, although
it does lead to these arbitrary decisions.
1. # (3 + 5 : float) ;;
Line 1, characters 1-6:
1 | (3 + 5 : float) ;;
^^^^^
Error: This expression has type int but an expression was expected
of type
float
2. # (3. + 5. : float) ;;
Line 1, characters 1-3:
1 | (3. + 5. : float) ;;
^^
Error: This expression has type float but an expression was
expected of type
int
3. # (3. +. 5. : float) ;;
- : float = 8.
4. # (3 : bool) ;;
Line 1, characters 1-2:
1 | (3 : bool) ;;
^
Error: This expression has type int but an expression was expected
of type
bool
444 PROGRAMMING WELL
5. # (3 || 5 : bool) ;;
Line 1, characters 1-2:
1 | (3 || 5 : bool) ;;
^
Error: This expression has type int but an expression was expected
of type
bool
6. # (3 || 5 : int) ;;
Line 1, characters 1-2:
1 | (3 || 5 : int) ;;
^
Error: This expression has type int but an expression was expected
of type
bool
Solution to Exercise 11 Since the unit type has only one value, there
is only one such typing:
() : unit
# sqrt true ;;
Line 1, characters 5-9:
1 | sqrt true ;;
^^^^
Error: This expression has type bool but an expression was expected
of type
float
Solution to Exercise 14 The most direct approach uses two let bind-
ing for the two sides:
Solution to Exercise 17
1. let x = 3 in
let y = 4 in
y * y ;;
2. let x = 3 in
let y = x + 2 in
y * y ;;
3. let x = 3 in
let y = 4 + (let z = 5 in z) + x in
y * y ;;
with a final value of price of 5.25. Thank goodness for strong static
typing, so that the R E P L was able to warn us of the error, rather than,
for instance, silently rounding the result or some such problematic
“correction” of the code.
# let area =
# let radius = 4. in
# let pi = 3.1416 in
# pi *. radius ** 2. ;;
val area : float = 50.2656
Solution to Exercise 20
1. 2 : int
2. 2 : int
4. "OCaml" : string
5. "OCaml" : string
# fun x -> x *. x ;;
- : float -> float = <fun>
# fun s -> s ^ s ;;
- : string -> string = <fun>
SOLUTIONS TO SELECTED EXERCISES 447
Solution to Exercise 22
Solution to Exercise 24
# let square (x : float) : float =
# x *. x ;;
val square : float -> float = <fun>
Solution to Exercise 25
# let abs (n : int) : int =
# if n > 0 then n else ~- n ;;
val abs : int -> int = <fun>
Solution to Exercise 33
Solution to Exercise 34
1. bool * int
2. bool * bool
3. int * int
4. float * int
5. float * int
6. int * int
Solution to Exercise 35
# true, true ;;
- : bool * bool = (true, true)
# true, 42, 3.14 ;;
- : bool * int * float = (true, 42, 3.14)
# (true, 42), 3.14 ;;
- : (bool * int) * float = ((true, 42), 3.14)
# (1, 2), 3, 4 ;;
- : (int * int) * int * int = ((1, 2), 3, 4)
# succ, 0, 42 ;;
- : (int -> int) * int * int = (<fun>, 0, 42)
# fun (f, n) -> 1 + f (1 + n) ;;
- : (int -> int) * int -> int = <fun>
Solution to Exercise 36
Solution to Exercise 38
# let snd (pair : int * int) : int =
# match pair with
# | _x, y -> y ;;
val snd : int * int -> int = <fun>
Solution to Exercise 39
# let addpair (x, y : int * int) : int =
# x + y ;;
val addpair : int * int -> int = <fun>
Solution to Exercise 41
1. # 3 :: [] ;;
- : int list = [3]
2. # true :: false ;;
Line 1, characters 8-13:
1 | true :: false ;;
^^^^^
Error: This variant expression is expected to have type bool list
The constructor false does not belong to type list
3. # true :: [false] ;;
- : bool list = [true; false]
4. # [true] :: [false] ;;
Line 1, characters 11-16:
1 | [true] :: [false] ;;
^^^^^
Error: This variant expression is expected to have type bool list
The constructor false does not belong to type list
5. # [1; 2; 3.1416] ;;
Line 1, characters 7-13:
1 | [1; 2; 3.1416] ;;
^^^^^^
Error: This expression has type float but an expression was
expected of type
int
7. # ([true], false) ;;
- : bool list * bool = ([true], false)
Solution to Exercise 43
It’s natural to return the additive identity 0 for the empty list to simplify
the recursion.
This function can also be implemented using the techniques of
Chapter 8 as a single fold.
Solution to Exercise 44
It’s natural to return the multiplicative identity 1 for the empty list to
simplify the recursion.
This function can also be implemented using the techniques of
Chapter 8 as a single fold.
Solution to Exercise 45
Solution to Exercise 46
Solution to Exercise 47
Solution to Exercise 48
# match x with
# | [] -> y
# | hd :: tl -> hd :: (append tl y) ;;
val append : int list -> int list -> int list = <fun>
Solution to Exercise 49
Solution to Exercise 50
If your definition was longer, you’ll want to review the partial applica-
tion discussion.
Solution to Exercise 51
Solution to Exercise 52
# let length lst = fold_left (fun tlval _hd -> 1 + tlval) 0 lst
# ;;
val length : int list -> int = <fun>
# let reduce (f : int -> int -> int) (list : int list) : int =
# match list with
# | hd :: tl -> List.fold_left f hd tl ;;
Lines 2-3, characters 0-36:
2 | match list with
3 | | hd :: tl -> List.fold_left f hd tl...
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val reduce : (int -> int -> int) -> int list -> int = <fun>
This approach has the disadvantage that applying reduce to the empty
list yields an unintuitive “Match failure” error message. Looking ahead
to Section 10.3 on handling such errors explicitly, we can raise a more
appropriate exception, the Invalid_argument exception.
# let reduce (f : int -> int -> int) (list : int list) : int =
# match list with
# | hd :: tl -> List.fold_left f hd tl
# | [] -> raise (Invalid_argument "reduce: empty list") ;;
val reduce : (int -> int -> int) -> int list -> int = <fun>
(You may want to revisit this latter solution after reading Chapter 9.)
The last two may be a bit confusing: Why ((<) 0) for the positives?
Don’t we want to accept only those that are greater than 0? The < func-
tion is curried with its prefix argument before its postfix argument, so
that the function ((<) 0) is equivalent to fun x -> 0 < x, that is,
the function that returns true for positive integers. Nonetheless, the
expression ((<) 0) doesn’t “read” that way, which is a good argument
for not being so cute and using instead the slightly more verbose but
transparent
# let positives = filter (fun n -> n > 0) ;;
val positives : int list -> int list = <fun>
# let negatives = filter (fun n -> n < 0) ;;
val negatives : int list -> int list = <fun>
# reverse [1; 2; 3] ;;
- : int list = [3; 2; 1]
Solution to Exercise 58
# let append (xs : int list) (ys : int list) : int list =
# List.fold_right cons xs ys ;;
Line 2, characters 16-20:
2 | List.fold_right cons xs ys ;;
^^^^
Error: Unbound value cons
Hint: Did you mean cos?
SOLUTIONS TO SELECTED EXERCISES 455
or even
where the explicit type annotation does the work. The structure of
the code does little (respectively, nothing) to manifest the requested
type.
A simple solution relies on the insight that the required type is just
the uncurried version of the type for the (&&) operator.
# let f (x, y) =
# x && y in
# f ;;
- : bool * bool -> bool = <fun>
let f xs =
match xs with
| [] -> ...
| h :: t -> ... in
f ;;
Now, we need to make sure the result type is bool list, taking care
not to further instantiate ’a. We can insert any values of the right
type as return values, but to continue the verisimilitude, we use the
empty list for the first case and a recursive call for the second. (Note
the added rec to allow the recursive call.)
# let rec f xs =
# match xs with
# | [] -> []
# | _h :: t -> true :: (f t) in
# f ;;
- : 'a list -> bool list = <fun>
# let f g a b =
# g (a, b) in
# f ;;
- : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
but this by itself does not guarantee that the result type of the func-
tion is ’a. Rather, f types as (’a * ’b -> ’c) -> ’a -> ’b ->
’c. (It’s the curry function from lab!) We can fix that by, say, com-
paring the result with a known value of the right type, namely a.
# let f g a b =
# if g (a, b) = a then a else a in
# f ;;
- : ('a * 'b -> 'a) -> 'a -> 'b -> 'a = <fun>
4. Again, we start with a let definition that just lays out the types of
the arguments in a pattern, and then make sure that each compo-
nent has the right type. One of many possibilities is
SOLUTIONS TO SELECTED EXERCISES 457
# let f x g = g x ;;
val f : 'a -> ('a -> 'b) -> 'b = <fun>
# ( |> ) ;;
- : 'a -> ('a -> 'b) -> 'b = <fun>
7. This question is deceptively simple. The trick here is that the func-
tion is polymorphic in both its inputs and outputs, yet the argu-
ments and return type may be different. In fact, we circumvent this
issue by simply not returning a value at all. There are two ways to
approach this:
# let map (f : 'a -> 'b) (lst : 'a list) : 'b list =
# fold (fun elt accum -> (f elt) :: accum)
# lst [] ;;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
# let f x =
# x +. 42. ;;
val f : float -> float = <fun>
# let f g x =
# g (x + 1) ;;
val f : (int -> 'a) -> int -> 'a = <fun>
SOLUTIONS TO SELECTED EXERCISES 459
3. The argument type for f, that is, the type of x, must be a list, say, ’a
list. The result type can be gleaned from the two possible return
values x and h. Since h is an element of x, it must be of type ’a.
Thus the return type is both ’a and ’a list. But there is no type
that matches both. Thus, the expression does not type.
# let f x =
# match x with
# | [] -> x
# | h :: t -> h ;;
Line 4, characters 12-13:
4 | | h :: t -> h ;;
^
Error: This expression has type 'a but an expression was expected
of type
'a list
The type variable 'a occurs inside 'a list
4. The result type for f must be the same as the type of a since it re-
turns a in one of the match branches. Since x is matched as a list, it
must be of list type. So far, then, we have f of type ... list -> ’a
-> ’a. The elements of x (such as h) are apparently functions, as
shown in the second match branch where h is applied to something
of type ’a and returning also an ’a; so h is of type’a -> ’a. The
final typing is f : (’a -> ’a) list -> ’a -> ’a.
# let rec f x a =
# match x with
# | [] -> a
# | h :: t -> h (f t a) ;;
val f : ('a -> 'a) list -> 'a -> 'a = <fun>
5. The match tells us that the first argument x is a pair, whose element
w is used as a bool; we’ll take the type of the element z to be ’a. The
second argument y is applied to z (of type ’a) and returns a bool
(since the then and else branches of the conditional tell us that y z
and w are of the same type). Thus the type of f is given by the typing
f : bool * ’a -> (’a -> bool) -> bool.
let f x y =
match x with
| (w, z) -> if w then y z else w ;;
same as the output type of x. The final typing is thus f : (’a ->
’a -> ’b) -> ’a -> ’b.
# let f x y =
# x y y ;;
val f : ('a -> 'a -> 'b) -> 'a -> 'b = <fun>
# let f x y =
# x (y y) ;;
Line 2, characters 5-6:
2 | x (y y) ;;
^
Error: This expression has type 'a -> 'b
but an expression was expected of type 'a
The type variable 'a occurs inside 'a -> 'b
8. The code matches x with option types formed with Some or None, so
we know that x must be of type ’a option for some ’a. We also see
that when deconstructing x into Some y, we perform subtraction
on y in the recursive function call: f (Some (y - 1)). We can thus
conclude y is of type int, and can further specify x to be of type
int option. Finally, note that the case None | Some 0 -> None
is the sole terminal case in this recursive function. Because this
case returns None, we know that if f terminates, f returns None. Our
function f therefore outputs a value of type ’a option. We cannot
infer a more specific type for ’a because we always return None and
thus have no constraints on ’a. The final typing is thus as follows: f
: int option -> ’a option.
# let rec f x =
# match x with
# | None
# | Some 0 -> None
# | Some y -> f (Some (y - 1)) ;;
val f : int option -> 'a option = <fun>
# let f x y =
# if x then [x]
# else [not x; y] ;;
val f : bool -> bool -> bool list = <fun>
let map (f : 'a -> 'b) (lst : 'a list) : 'b list =
List.fold_right (fun elt accum -> f elt :: accum)
lst [] ;;
let map (f : 'a -> 'b) (lst : 'a list) : 'b list =
List.fold_left (fun accum elt -> accum @ [f elt])
[] lst ;;
let map (f : 'a -> 'b) : 'a list -> 'b list =
List.fold_left (fun accum elt -> accum @ [f elt]) [] ;;
Solution to Exercise 66
# let rec interleave (n : 'a) (lst : 'a list)
# : 'a list list =
# match lst with
# | [] -> [[n]]
# | x :: xs -> (n :: x :: xs)
# :: List.map (fun l -> x :: l)
# (interleave n xs) ;;
val interleave : 'a -> 'a list -> 'a list list = <fun>
Solution to Exercise 69
# let rec odds (lst : 'a list) : 'a list =
# match lst with
# | [] -> []
# | [a] -> [a]
# | a :: _b :: rest -> a :: odds rest ;;
val odds : 'a list -> 'a list = <fun>
Solution to Exercise 73
# let luhn (nums : int list) : int =
# let s = sum ((List.map doublemod9 (odds nums))
# @ (evens nums)) in
# 10 - (s mod 10) ;;
val luhn : int list -> int = <fun>
Alternatively, the check could have been done inside the second match
statement. Why might this be the dispreferred choice?
Solution to Exercise 78
# let rec last_opt (lst : 'a list) : 'a option =
# match lst with
# | [] -> None
# | [elt] -> Some elt
# | _ :: tl -> last_opt tl ;;
val last_opt : 'a list -> 'a option = <fun>
464 PROGRAMMING WELL
Solution to Exercise 83
Solution to Exercise 85
1. # let f x y =
# Some (x + y) ;;
val f : int -> int -> int option = <fun>
2. # let f g =
# Some (1 + g 3) ;;
val f : (int -> int) -> int option = <fun>
3. # let f x g = g x ;;
val f : 'a -> ('a -> 'b) -> 'b = <fun>
or
# let f = ( |> ) ;;
val f : 'a -> ('a -> 'b) -> 'b = <fun>
4. # let rec f xl yl =
# match xl, yl with
# | (Some xhd :: xtl), (Some yhd :: ytl)
# -> (xhd, yhd) :: f xtl ytl
# | (None :: _), _
# | _, (None :: _)
# | [], _
# | _, [] -> [] ;;
val f : 'a option List.t -> 'b option List.t -> ('a * 'b) List.t =
<fun>
Solution to Exercise 86
2. The type of f is bool -> bool * bool. In fact, f always returns the
same value, the pair true, true.
466 PROGRAMMING WELL
Note that the explicit typing of b is required to force the function type
to be bool -> bool * bool instead of ’a -> bool * bool.
# ( |> ) ;;
- : 'a -> ('a -> 'b) -> 'b = <fun>
Solution to Exercise 96
Solution to Exercise 97 There are only six card types, so one might be
inclined to just have an enumerated type with six constructors:
type card =
| KSpades
| QSpades
| JSpades
| KDiamonds
| QDiamonds
| JDiamonds ;;
Note that the field names and type names can be identical, since they
are in different namespaces.
Using ints for the suits and card values, for instance,
is inferior as the convention for mapping between int and card suit
or value is obscure. At best it could be made clear in documentation,
but the enumerated type makes it clear in the constructors themselves.
Further, the int approach allows ints that don’t participate in the
mapping, and thus doesn’t let the language help with catching errors.
We have carefully ordered the constructors from better to worse
and ordered the record components from higher to lower order so that
comparisons on the data values will accord with the “better” relation,
as seen in the solution to Problem 99.
This relies on the fact that the < operator has a kind of ad hoc poly-
morphism, which works on enumerated and variant types, pairs, and
records inductively to define an ordering on values of those types.
Relying on this property of variant types behooves you to explicitly
document the fact at the type definition so it gets preserved.
To not rely on the ad hoc polymorphism of <, we need a more ex-
plicit definition like this:
Solution to Exercise 104 Let’s start with the tree input and the output
of the tree traversal. The third argument to foldbt is a binary tree of
type ’a bintree, say. The result of the traversal is a value of type, say,
’b. Then the first argument, which serves as the return value for empty
trees must also be of type ’b and the function calculating the values for
internal nodes is given the value stored at the node (’a) and the two
recursively returned values and returns a ’b; it must be of type ’a ->
’b -> ’b -> ’b. Overall, the appropriate type for foldbt is
'b -> ('a -> 'b -> 'b -> 'b) -> 'a bintree -> 'b
# let sum_bintree =
# foldbt 0 (fun v l r -> v + l + r) ;;
val sum_bintree : int bintree -> int = <fun>
# preorder int_bintree ;;
- : int list = [16; 93; 3; 42]
Solution to Exercise 111 What we were looking for here is the proper
definition of a functor named MakeImaging taking an argument, where
the functor and argument are appropriately signature-constrained.
Typical problems are to leave out the : PIXEL, the : IMAGING, or the
sharing constraint.
module DiscreteTimeInterval =
MakeInterval (DiscreteTime) ;;
let intersection i j =
if relation i j = Disjoint then None
else let (x, y), (x', y') = endpoints i, endpoints j in
Some (interval (max x x') (min y y')) ;;
Solution to Exercise 118 There are myriad solutions here. The idea is
just to establish a few intervals and then test that you can recover some
endpoints or relations. Here are a few possibilities:
open Absbook ;;
let test () =
let open DiscreteTimeInterval in
let i1 = interval 1 3 in
let i2 = interval 2 6 in
let i3 = interval 0 7 in
let i4 = interval 4 5 in
unit_test (relation i1 i4 = Disjoint) "disjoint\ n";
unit_test (relation i1 i2 = Overlaps) "overlaps\ n";
unit_test (relation i1 i3 = Contains) "contains\ n";
unit_test
(relation (union i1 i2) i4 = Contains) "unioncontains\ n";
let i23 = intersection i1 i2 in
un
it_test (let
Some e23 = i23 in endpoints e23 = (2, 3)) "intersection";;
print_endline "tests completed" ;;
Solution to Exercise 119 Since we only need the float functionality for
weight, a simple definition is best.
# type shape =
# | Circle
# | Oval
# | Fin ;;
type shape = Circle | Oval | Fin
Solution to Exercise 121 Since we want each object to have two at-
tributes – a weight AND a shape – we want to use conjunction here. We
can construct a record type obj to represent objects. This allows us to
ensure each object has a weight and shape that are of the appropriate
type.
# type obj = { weight : weight; shape : shape } ;;
type obj = { weight : weight; shape : shape; }
It should be
let mobile1 =
let open Mobile in
make_node
1.0
(make_leaf {shape = Oval; weight = 9.0})
(make_node
1.0
(make_leaf {shape = Fin; weight = 3.5})
(make_leaf {shape = Fin; weight = 4.5})) ;;
let size =
Mobile.walk (fun _leaf -> 1)
(fun _node left_size right_size ->
left_size + right_size) ;;
We’re told we want to use the walk function here. Since the walk func-
tion does the hard work of traversing the Mobile.tree for us, we just
need to pass in the proper arguments to walk in order to construct
the function shape_count. The walk function is of type (leaft ->
’a) -> (nodet -> ’a -> ’a -> ’a) -> tree -> ’a and takes in
two functions, one specifying behavior for leaves and one for nodes.
If we can define these two functions, we can easily define shape. Let’s
start with the function that specifies how we want to count leaves; we
need a function of type leaf -> ’a. The shape_count of a single leaf
should be 1 if the leaf matches the desired shape s and 0 otherwise. We
can construct an anonymous function that achieves this functionality
as follows:
fun leaf -> if leaf.shape = s then 1 else 0
We now want to address the case of nodes. Nodes don’t have shapes
themselves, but rather connect to other subtrees that might. To find
the shape count of a node, we just need to add the shape counts of its
subtrees.
fun _node l r -> l + r ;;
Solution to Exercise 130 Again, we can use the walk function here
to avoid traversing the tree directly. We will again need to come up
with two functions to pass into walk, one for the leaves and one for the
nodes. Let’s look at the base case, leaves. A leaf is always balanced, so
we just ned to return Some w, where w is the weight of the leaf.
SOLUTIONS TO SELECTED EXERCISES 475
Now, let’s look at the nodes. We want a function of the form nodet ->
’a -> ’a -> ’a, where the first argument is the node itself and the
remaining two are the results of walk on the left subtree and walk on
the right subtree, respectively. We want to ensure our node is balanced:
this requires that the left and right subtrees are each balanced and are
of equal weight. If these conditions are met we want to return If the
subtrees aren’t balanced or are of unequal weight, we want to return
Some w, where w is the sum of the weights of the connector and its
subtrees. We return None otherwise.
let balanced =
Mobile.walk (fun leaf -> Some leaf.weight)
(fun node l r ->
match l, r with
| Some wt1, Some wt2 ->
if wt1 = wt2 then
Some (node +. wt1 +. wt2)
else None
| _, _ -> None) ;;
Tod d s (n ) = c + Tod d s (n − 2)
Solution to Exercise 169 Let’s start with two mutable values of type
int list ref that are structurally equal but physically distinct:
Now for two values that are physically equal (that is, aliases), and
therefore structurally equal as well:
# let lstref3 = ref [1; 2; 3] ;;
val lstref3 : int list ref = {contents = [1; 2; 3]}
# let lstref4 = lstref3 ;;
val lstref4 : int list ref = {contents = [1; 2; 3]}
# lstref3 = lstref4 ;;
- : bool = true
# lstref3 == lstref4 ;;
- : bool = true
1. # let a = ref 3 in
# let b = ref 5 in
# let a = ref b in
# !(!a) ;;
Line 1, characters 4-5:
1 | let a = ref 3 in
^
Warning 26: unused variable a.
- : int = 5
3. Note the warning that the inner definition of a is not used; the a
used in the definition of b is the outer one, as required by lexical
scoping. (The R E P L even reports that the inner b is unused.)
# let a = ref 1 in
# let b = ref a in
# let a = ref 2 in
# !(!b) ;;
Line 3, characters 4-5:
3 | let a = ref 2 in
^
Warning 26: unused variable a.
- : int = 1
4. # let a = 2 in
# let f = (fun b -> a * b) in
# let a = 3 in
# f (f a) ;;
- : int = 12
# let p = ref 11 ;;
val p : int ref = {contents = 11}
# let r = ref p ;;
val r : int ref ref = {contents = {contents = 11}}
# p ;;
- : int ref = {contents = 11}
(b) True. The explanation here is the same as for (1): Since s is a
reference to !r, it’s of type int ref ref, the type of r.
# r ;;
- : int ref ref = {contents = {contents = 11}}
# s ;;
- : int ref ref = {contents = {contents = 11}}
# s ;;
- : int ref ref = {contents = {contents = 11}}
(d) True. We see r and s are a reference to the same value – that
is, they both are references to p – they therefore are structurally
equivalent.
# r ;;
- : int ref ref = {contents = {contents = 11}}
# s ;;
- : int ref ref = {contents = {contents = 11}}
# let t =
# !s := 14;
# !p + !(!r) + !(!s) ;;
val t : int = 42
# t ;;
- : int = 42
5. Note how similar the code in 7–9 looks to the code in 4–6. Yet there
is in fact one key difference: we’re changing s itself rather than
!s. This means that instead of modifying our reference to p, we’re
replacing it. With the line s := ref 17, we’re declaring an entirely
new reference that points to an instance of the value 17, and setting
s to point to that reference. This effectively severs the tie between s
and p: s points to a to a completely separate reference to a block of
memory containing the value 17, while p continues to point to the
value 14.
As for r, note that while s and r started out structurally equivalent,
they were never physically equivalent. Think back to when we
defined s:
let s = ref !r ;;
When we dereference r with !r, we lose all association with the spe-
cific block of memory to which r refers and are only passed along
the value contained in that block. Thus while s is also a reference
to the value r references – that is, both s and r are references to p –
s and r are in fact distinct references pointing to distinct blocks in
482 PROGRAMMING WELL
# let t =
# s := ref 17;
# !p + !(!r) + !(!s) ;;
val t : int = 45
# t ;;
- : int = 45
Nil})})
# let Cons(_h, t) = a in
# let b = Cons(1, ref a) in
# t := b;
# mhead (mtail (mtail b)) ;;
Lines 1-4, characters 0-23:
1 | let Cons(_h, t) = a in
2 | let b = Cons(1, ref a) in
3 | t := b;
4 | mhead (mtail (mtail b))...
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Nil
- : int = 1
# let rec first (n: int) (mlst: 'a mlist) : 'a list =
# if n = 0 then []
# else match mlst with
# | Nil -> []
# | Cons(hd, tl) -> hd :: first (n-1) !tl ;;
val first : int -> 'a mlist -> 'a list = <fun>
Next, we can look at the member function. Using the same approach,
we get
let member dct target =
let rec member' loc =
(* fallen off the end of the array; not found *)
if loc >= D.size then false
else
match dct.(loc) with
| Empty ->
(* found an empty slot; target not found *)
false
| Element {key; _} ->
if key = target then
Perhaps you see the problem. The code is nearly identical, once the
putative location for the target key is found. The same will be true for
lookup and remove. Rather than reimplement this search process in
each of the functions, we can abstract it into its own function, which
we’ll call findloc. This function returns the (optional) location (index)
where a particular target key is already or should go, or None if no such
location is found.
Notice that the constructors in the patterns, which are merely used
to deconstruct values, are unchanged. Only the instances used to
construct new values are replaced with their metered counterparts.
488 PROGRAMMING WELL
With the metered version in hand, we can see the allocations more
clearly.
# Metered.reset () ;;
- : unit = ()
# MeteredQuickSort.sort (<)
# [1; 3; 5; 7; 9; 2; 4; 6; 8; 10] ;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; ...]
# Metered.count () ;;
- : int = 92
SOLUTIONS TO SELECTED EXERCISES 489
Now we can generate the stream of ratios for the Fibonacci sequence
and find the required approximation:
Note the use of the smap function and the use of partial application.
# let circand : bool stream -> bool stream -> bool stream =
# smap2 (&&) ;;
val circand : bool stream -> bool stream -> bool stream = <fun>
# let circnand (s: bool stream) (t: bool stream) : bool stream =
# circnot (circand s t) ;;
val circnand : bool stream -> bool stream -> bool stream = <fun>
# let mono x = [x + 1] ;;
val mono : int -> int list = <fun>
# let poly x = [x] ;;
val poly : 'a -> 'a list = <fun>
# let need f =
# match f 3 with
# | [] -> []
# | hd :: tl -> hd + 1 :: tl ;;
val need : (int -> int list) -> int list = <fun>
# need mono ;;
- : int list = [5]
# need poly ;;
- : int list = [4]
Solution to Exercise 209 The solution here makes good use of inheri-
tance rather than reimplementation.
SOLUTIONS TO SELECTED EXERCISES 491
{} ⊢ let x = 3 in let y = 5 in x + y
⇓
RRR {} ⊢ 3 ⇓ 3
RRR
RRR {x ↦ 3} ⊢ let y = 5 in x + y
RRR
RRR ⇓
RRR
RRR RRR {x ↦ 3} ⊢ 5 ⇓ 5
RRR RRR
RRR RRR {x ↦ 3; y ↦ 5} ⊢ x + y
RRR RRR
RRR RRR ⇓
RRR RRR
RRR RRR {x ↦ 3; y ↦ 5} ⊢ x ⇓ 3
RRR RRR ∣
RRR RRR {x ↦ 3; y ↦ 5} ⊢ y ⇓ 5
RRR RRR
RRR RRR ⇓8
RRR
RR ⇓ 8
⇓8
492 PROGRAMMING WELL
{} ⊢ let x = 3 in let x = 5 in x + y
⇓
RRR {} ⊢ 3 ⇓ 3
RRR
RRR {x ↦ 3} ⊢ let x = 5 in x + x
RRR
RRR ⇓
RRR
RRR RRR {x ↦ 3} ⊢ 5 ⇓ 5
RRR RRR
RRR RRR {x ↦ 5} ⊢ x + x
RRR RRR
RRR RRR ⇓
RRR RRR
RRR R
RRR {x ↦ 5} ⊢ x ⇓ 5
RRR RRR ∣
RRR RRR {x ↦ 5} ⊢ x ⇓ 5
RRR RRR
RRR RR ⇓ 10
RRR
RR ⇓ 10
⇓ 10
# let a = 2 in
# let f = (fun b -> a * b) in
# let a = 3 in
# f (f a) ;;
- : int = 12
494 PROGRAMMING WELL
E ⊢ if C then T else F ⇓
E ⊢ C ⇓ true
∣ (R ifthen )
E ⊢ T ⇓ vT
⇓ vT
E ⊢ if C then T else F ⇓
E ⊢ C ⇓ false
∣ (R ifelse )
E ⊢ F ⇓ vF
⇓ vF
E,S ⊢ ! P ⇓
∣ E , S ⊢ P ⇓ l , S′ (R deref )
⇓ S ′ (l ), S ′
E,S ⊢ P ; Q ⇓
E , S ⊢ P ⇓ (), S ′
∣ (R seq )
E , S ′ ⊢ Q ⇓ vQ , S ′′
⇓ vQ , S ′′
SOLUTIONS TO SELECTED EXERCISES 495
let rec x = D in B
to be equivalent to
′ ′
let x = ref unassigned in (x := D ); B
E , S ⊢ let x = ref U in (x := D ′ ); B ′
⇓
RRR E , S ⊢ ref U
RRR
RRR ⇓
RRR
RRR ∣ E,S ⊢U ⇓U,S
RRR
RRR ⇓ l , S {l ↦ U }
RRR
RRR E {x ↦ l }, S {l ↦ U } ⊢ (x := D ′ ); B ′
RRR
RRR ⇓
RRR RRR E {x ↦ l }, S {l ↦ U } ⊢ x := D ′
RRR RRR
RRR RRR
RRR RRR ⇓
RRR R RRR E {x ↦ l }, S {l ↦ U } ⊢ x ⇓ l , S {l ↦ U }
RRR RRR RRR
RRR RRR RRR E {x ↦ l }, S {l ↦ U } ⊢ D’ ⎫
RRR RRR RRR ⎪
⎪
RRR RRR ⎪
⎪
⎪
RRR R R
R
RRR ⇓ ⎪
⎪
RRR ⎬
RRR RRR R
R ∣ ⋯ ⎪
RRR RRR RRR ⎪
⎪
RRR RRR RRR ⎪
⎪
⎪
RRR RRR R
R ⇓ v D , S ′ ⎪
⎭
RRR RRR ⇓ , S ′
{ l ↦ v }
RRR RRR () D
RRR RRR E {x ↦ l }, S ′ {l ↦ v D } ⊢ B’ ⎫
⎪
RRR RRR ⎪
⎪
⎪
RRR RRR ⇓ ⎪
⎪
⎪
RRR RRR ⎬
RRR RRR ∣ ⋯ ⎪
⎪
RRR RRR ⎪
⎪
⎪
RRR RRR ⇓ vB , S ′′ ⎪
⎪
RRR ⎭
RRR ⇓ vB , S ′′
⇓ v B , S ′′
tions as premises:
E , S ⊢ let rec x = D in B ⇓
E {x ↦ l }, S {l ↦ unassigned} ⊢ D [x ↦ !x ] ⇓ v D , S ′
∣
E {x ↦ l }, S ′ {l ↦ v D } ⊢ B [x ↦ !x ] ⇓ v B , S ′′
⇓ v B , S ′′
(R letrec )
# let sum_bintree =
# foldbt_conc 0 (fun v l r -> v + l + r) ;;
val sum_bintree : int bintree -> int = <fun>
# let int_bintree =
# Node (16, Node (93, Empty, Empty),
# Node (3, Node (42, Empty, Empty),
# Empty)) ;;
val int_bintree : int bintree =
Node (16, Node (93, Empty, Empty),
Node (3, Node (42, Empty, Empty), Empty))
# sum_bintree int_bintree ;;
- : int = 154
# let res =
# try f ()
# with exn -> Mutex.unlock l;
# raise exn in
# Mutex.unlock l;
# res ;;
val with_lock : Mutex.t -> (unit -> 'a) -> 'a = <fun>
Bibliography
Eriola Kruja, Joe Marks, Ann Blair, and Richard Waters. A short note on
the history of graph drawing. In International Symposium on Graph
Drawing, pages 272–286. Springer, 2001.
Harry Lewis and Rachel Zax. Essential Discrete Mathematics for Com-
puter Science. Princeton University Press, Princeton, New Jersey,
2019.
Note: The page numbers for primary occurrences of indexed items are typeset as 123, other occurrences as
123.