Abstraction
Abstraction
SHIEBER
PROGRAMMING
WELL:
ABSTRACTION AND
DESIGN IN
C O M P U TAT I O N
©2023 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 1df7fe2 from Thu Aug 22 15:37:30 2024 -0400 by Stuart Shieber.
1
Contents
Preface 13
1 Introduction 19
1.1 An extended example: greatest common divisor . . . . . 21
1.2 Programming as design . . . . . . . . . . . . . . . . . . . . 24
1.3 The OCaml programming language . . . . . . . . . . . . . 26
1.4 Tools and skills for design . . . . . . . . . . . . . . . . . . 28
6 Functions 59
6.1 Function application . . . . . . . . . . . . . . . . . . . . . 60
6.2 Multiple arguments and currying . . . . . . . . . . . . . . 61
6.3 Defining anonymous functions . . . . . . . . . . . . . . . 62
6.4 Named functions . . . . . . . . . . . . . . . . . . . . . . . 63
6.4.1 Compact function definitions . . . . . . . . . . . 64
6.4.2 Providing typings for function arguments and
outputs . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.5 Function abstraction and irredundancy . . . . . . . . . . 67
6.6 Defining recursive functions . . . . . . . . . . . . . . . . . 69
6.7 Unit testing . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.8 Supplementary material . . . . . . . . . . . . . . . . . . . 76
20 Concurrency 343
20.1 Sequential, concurrent, and parallel computation . . . . 344
20.2 Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . 345
20.3 Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
20.4 Interthread communication . . . . . . . . . . . . . . . . . 349
20.5 Futures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
20.6 Futures are not enough . . . . . . . . . . . . . . . . . . . . 354
20.7 Locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
20.7.1 Abstracting lock usage . . . . . . . . . . . . . . . . 359
20.8 Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
Bibliography 475
Index 479
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 ably introduces some half of all Har-
vard undergraduate students to computer programming, and in its
online HarvardX version CS50x has benefited hundreds of thousands
of other students.
Students just learning to program, like those in CS50, typically view
the end product of programming as a program that works – that “gets
the right answer”. Once such a program is in hand, the student thinks,
the programmer’s job is done. This book was developed to move stu-
dents past this view of programming, to focus on programming well,
regarding programming not as a transaction but as an art and a craft.
The book emphasizes the role of abstraction and abstraction mech-
anisms in engendering a design space in which good programs can be
constructed. These abstraction mechanisms are associated with and
enable the major programming paradigms – first- and higher-order
functional programming, structure-driven programming, generic pro-
gramming, modular programming, imperative programming, proce-
dural programming, lazy programming, object-oriented programming,
and concurrent programming. By expanding the student’s armamen-
tarium of abstraction mechanisms, this design space grows as well,
making possible programs that are better along multiple dimensions
– readability, maintainability, succinctness, efficiency, testability, and, The programming edicts:
most importantly but ineffably, beauty. • Edict of intention: Make your
intentions clear.
• Edict of irredundancy: Never write
Aims the same code twice.
• Edict of decomposition: Carve
In developing the book, I had in mind several aims. software at its joints.
• Edict of prevention: Make the illegal
inexpressible.
Explicit presentation of general principles. I introduce a small set of
• Edict of compartmentalization:
very general software engineering principles – presented as “edicts”
Limit information to those with a
in the text – and make frequent reference to them throughout the need to know.
text to tie together more particular software engineering ideas.
14 PROGRAMMING WELL
Use of OCaml
Clean semantics. The language has quite clean semantics, which aids
understanding.
Limitations
• It does not cover the OCaml language exhaustively, and does not
serve as a language reference. This is in keeping with the use of
OCaml as a vehicle for presenting concepts. Just enough OCaml is
presented to make possible the implementations of the presented
concepts. (Cf. Minsky et al.’s Real World OCaml.)
Acknowledgements
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 21
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,
INTRODUCTION 25
• beauty.
between the lambda calculus and the Turing machine mirrors the close
relationship between Church and Turing; Church was Turing’s PhD
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
tured in this book; their salariesErlang
are substantially higher on average.
115000 Scala
OCaml
OCaml
Scalaaren’t the point of this book, they
Although such pecuniary benefits 115000
Clojure
OCaml 114000 Go
certainly don’t hurt. Groovy
Clojure 110000
Objective-C
Go 110000 F#
Groovy 110000 Hack
1.4 Tools and skills for design Perl
Objective-C 110000
Kotlin
F# 108000 Rust
The space of design options available to you is enabled by the palette
108000
Swift
Hack
TypeScript
of abstraction mechanisms thatPerl
you can fluently deploy.
106000 Navigating Bash/Shell
Kotlin 105000 CoffeeScript
the design space to find the best solutions is facilitated by a set of skills ObjectPascal
Rust 105000
and analytic tools, which we will also introduce throughout the follow- Haskell
Swift 102000 Java
ing chapters as they become pertinent.
TypeScript These include more precise
102000
Lua
Ruby
notions of the syntax and semantics of programming
Bash/Shell languages, fa-
100000 Julia
100000 C
cility with notations, sensitivityCoffeeScript
to programming style (see especially JavaScript
ObjectPascal 100000
Python
Appendix C), programming interface
Haskell
design, unit testing,
100000
tools (big- 90K 95K 100K 105K 110K 115K 120K
O notation, recurrence equations)
Java for analyzing efficiency
100000 of code.
Having these tools and skills at Lua 100000
your disposal will add to your computa- Figure 1.8: United States average salary
Ruby 100000 by technology, from StackOverflow
tional tool-box and stretch your thinking about what98500
it means to write Developer Survey 2018. Highlighted
Julia
good code. I expect, based on my
C own experiences, 98000
that learning to bars correspond to technologies in the
JavaScript 98000precision will typed functional family.
develop, analyze, and express your software ideas with
Python 98000
also benefit your abilities to develop, analyze, and express ideas more
generally.
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
30 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 -
E VA L - P R I N T L O O P ” or R E P L .2 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
alternative right-hand sides. The BNF notation allows separating
alternative 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
34 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 35
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.
36 PROGRAMMING WELL
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 37
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〉
for unary operators like ~-, the unary negation operator.
1. (~- 4) + 6
2. ~- (4 + 6)
3. 20 / ~- 4 + 6
4. 5 * (3 + 4)
5. ((4 + 3) * 5)
6. (3 + 4 + 0) * 5
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.
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: (* 〈〉 *).5 The primary purpose of comments is satisfying 5
We use the symbol 〈〉 here and
throughout the later chapters as a
the edict of intention. Comments should therefore describe the why
convenient notation to indicate un-
rather than the how of a program. Section C.2 presents some useful specified text of some sort, a textual
stylistic considerations in providing comments for documenting pro- anonymous variable of a sort. Here,
it stands in for the text that forms the
grams. comment. In other contexts it stands in
There are other aspects of concrete syntax that can be freely de- for the arguments of an operator, con-
structor, or subexpression, for instance,
ployed because they have no affect on the computation that a program
in 〈〉 + 〈〉 or 〈〉 list or let 〈〉 in 〈〉 .
carries out. These too can be judiciously deployed to help express your
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 39
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 43
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).
p
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 B.3 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 is structured as
follows:4 4
We describe the syntax of the construct
using the BNF rule notation introduced
〈expr〉 ::= if 〈exprtest 〉 then 〈exprtrue 〉 else 〈exprfalse 〉 in Chapter 3. We will continue to do
so throughout as we introduce new
constructs of the language.
The value of such an expression is the value of the 〈exprtrue 〉 if the value As mentioned in footnote 2 on
of the test expression 〈exprtest 〉 is true and the value of the 〈exprfalse 〉 if page 34, in defining expression classes
using this notation, we use subscripts
the value of 〈exprtest 〉 is false.
to differentiate among different occur-
# if 3 = 3 then 0 else 1 ;; rences of the same expression class,
as we have done here with the three
- : int = 0
instances of the 〈expr〉 class – 〈exprtest 〉,
# 2 * if 3 > 4 then 3 else 4 + 5 ;;
〈exprtrue 〉, and 〈exprfalse 〉.
- : int = 18
# 2 * (if 3 > 4 then 3 else 4) + 5 ;;
- : int = 13
Programmers using a language with strong static typing for the first
time often find the frequent type errors limiting and even annoying.
46 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)
# 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.
VA L U E S A N D T Y P E S 47
• 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
48 PROGRAMMING WELL
5. 3 || 5 : bool
6. 3 || 5 : int
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.
OCaml is a F U N C T I O N A L P R O G R A M M I N G L A N G UA G E , by which we
mean more than that functions play a central role in the language. We
mean that functions are F I R S T- C L A S S VA L U E S – they can be passed as
arguments to functions or returned as the value of functions. Func-
tions that take functions as arguments or return functions as values
are referred to as H I G H E R - O R D E R F U N C T I O N S , and the powerful pro-
gramming paradigm that makes full use of this capability, which we
will introduce in Chapter 8, is H I G H E R - O R D E R F U N C T I O N A L P R O -
G R A M M I N G.
Related to the idea that functions are values is that they have types
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
What are the types of the three functions – succ, string_of_int, and not – from
Figure 4.3?
Exercise 13
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, (and again following the edict of inten-
tion), 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 inten-
tion. 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.
# 3.1416 *. (let radius = 2.
# in radius *. radius) ;;
- : float = 12.5664
Exercise 14
Are the parentheses necessary in this example? Try out the expression without the
parentheses and see what happens.
NAMING AND SCOPE 53
Exercise 15
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
lations making use of them repeat as well. Lines 1–4 and 10–13 both
separately calculate the area of the left triangle in the figure, and lines
5–8 and 15–18 calculate the area of the right triangle. The calculations
are redundant, and worse, provide the opportunity for bugs to creep in
if the copies aren’t kept in perfect synchrony.
Appropriate use of naming can partially remedy these problems.
(We’ll address their solution more systematically in Chapters 6 and 8.)
First, by naming the two area calculations, we need calculate each only
once.
# let left_area = sqrt ( ((1. +. 1. +. 1.41) /. 2.)
# *. ((1. +. 1. +. 1.41) /. 2. -. 1.)
# *. ((1. +. 1. +. 1.41) /. 2. -. 1.)
# *. ((1. +. 1. +. 1.41) /. 2. -. 1.41) ) in
# let right_area = sqrt ( ((1.5 +. 0.75 +. 2.) /. 2.)
# *. ((1.5 +. 0.75 +. 2.) /. 2. -. 1.5)
# *. ((1.5 +. 0.75 +. 2.) /. 2. -. 0.75)
# *. ((1.5 +. 0.75 +. 2.) /. 2. -. 2.) ) in
# if left_area > right_area then left_area else right_area ;;
- : float = 0.499991149296665216
We also correct a bug in line 7, which you may not have noticed, that
uses inconsistent values for one of the side lengths in the area calcula-
tions. By defining the area once and using the value twice, we remove
the possibility for such inconsistencies to even arise.
Finally, notice the repeated calculation of, for instance, (1. +. 1.
+. 1.41) /. 2., which is calculated some four times, and similarly
for (1.5 +. 0.75 +. 2.) /. 2.. Each of these is the S E M I P E R I M E -
TER of a triangle (that is, half the perimeter). The semiperimeter fea-
tures heavily in Heron’s method of calculating triangle areas. By nam-
ing these two subexpressions, we clarify even further what is going on
in the example.
# let left_area =
# let left_sp = (1. +. 1. +. 1.41) /. 2. in
# sqrt ( left_sp
# *. (left_sp -. 1.)
# *. (left_sp -. 1.)
# *. (left_sp -. 1.41) ) in
# let right_area =
# let right_sp = (1.5 +. 0.75 +. 2.) /. 2. in
# sqrt ( right_sp
# *. (right_sp -. 1.5)
# *. (right_sp -. 0.75)
# *. (right_sp -. 2.) ) in
# if left_area > right_area then left_area else right_area ;;
- : float = 0.499991149296665216
5.5 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 region 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:
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 16
Correct the example to provide the triple concatenation of the defined string.
Exercise 17
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.6.
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.
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:
56 PROGRAMMING WELL
# 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 ;;
Exercise 18
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.
NAMING AND SCOPE 57
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 ;;
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.)
Exercise 19
In the sequence of expressions
58 PROGRAMMING WELL
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 20
How might you get the effect of this definition of a global variable area by making use of
local variables for pi and radius?
Edict of irredundancy:
Never write the same code twice.
# succ 41 ;;
- : int = 42
FUNCTIONS 61
# 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 succ has the type given by the type
expression int -> int and the string_of_int function the type int
-> string.
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
was first discussed by Schönfinkel (1924), it is referred to as C U R RY I N G
a function, the resulting function being curried, so named after Haskell
Curry who popularized the approach.
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-
Figure 6.2: Haskell Curry (1900–1982),
argument function above can be expressed as
American logician, promulgator of
the use of higher-order functions
int -> int -> int -> int
to simulate functions of multiple
arguments, which is referred to as
Application, conversely, is left associative, so that applying a curried currying in his honor.
function f to its arguments can be notated f 1 2 3 instead of ((f 1)
2) 3.
62 PROGRAMMING WELL
# (+) ;;
- : int -> int -> int = <fun>
# (/.) ;;
- : float -> float -> float = <fun>
# (&&) ;;
- : bool -> bool -> bool = <fun>
Exercise 21
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 2.
The keyword fun introduces the function definition. The arrow ->
separates the typing of a variable that represents the input, the integer
x, from an expression that represents the output value, 2 * x. The
output expression can, of course, make free use of the input variable as
part of the computation.
FUNCTIONS 63
# fun x -> 2 * x ;;
- : int -> int = <fun>
Exercise 22
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:
Here are functions for the circumference and area of circles of given
radius:
64 PROGRAMMING WELL
# 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 -
TA C T I C S U G A R ,4 a bit of additional syntax that serves to abbreviate 4
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
FUNCTIONS 65
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
66 PROGRAMMING WELL
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 23
Consider the following beginnings of function declarations. How would these appear
using the compact notation (using whatever argument variable names you prefer)?
3. let foo : (float -> int) -> float -> bool = ...
Exercise 24
What are the types for the following expressions?
Exercise 25
Define a function square that squares a floating point number. For instance,
# square 3.14 ;;
- : float = 9.8596
# square 1234567. ;;
- : float = 1524155677489.
Exercise 26
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 27
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 28
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 29
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 28. Figure 6.3: The frustrum of a cone,
with top and bottom radii r 1 and r 2
respectively, and height h.
FUNCTIONS 67
Exercise 30
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 2
V= (r + r 1 r 2 + r 22 ) .
3 1
Implement a function to calculate the volume of a frustrum given the radii and height.
Problem 31
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 :
a = Y mod 19 h = (19a + b − d − g + 15) mod 30
Y c
b= i=
100 4
c = Y mod 100 k = c mod 4
b
d= l = (32 + 2e + 2i − h − k) mod 7
4
a + 11h + 22l
e = b mod 4 m=
451
b +8 h + l − 7m + 114
f = month =
25 31
b − f +1
g= day = ((h + l − 7m + 114) mod 31) + 1
3
Write two functions, computus_month and computus_day, which take an integer year
argument and return, respectively, the month and day of Easter as calculated by the
method above. Use them to verify that the date of Easter in 2018 was April 1.
calculation of the semiperimeter for the three sides and then the area
calculation itself, again using the three side lengths in corresponding
places.
Of course, they are not strictly identical; if they were, we could just
use the naming trick (Section 5.4) to remove the redundancy. How-
ever, except for the three side lengths, the two calculations are the
same. The two area values involve the same computation over the side
lengths, the same mapping from side lengths to area, the same func-
tion of the side lengths so to speak. We can view these two dissimilar
expressions as manifesting an underlying identity by thinking of them
as applications of one and the same function (call it area) to the three
side lengths.
We start with a definition of this area function.
# let area x y z =
# let sp = (x +. y +. z) /. 2. in
# sqrt (sp *. (sp -. x) *. (sp -. y) *. (sp -. z)) ;;
val area : float -> float -> float -> float = <fun>
of simple values (like in the area example) is not sufficient. The true
power of functions comes in with these even more sophisticated cases,
which we explore in detail in Chapter 8.
To prepare for those abstraction techniques, we extend the expres-
sivity of functions even further by allowing functions to be defined in
terms of themselves, recursive functions.
n ! = n · (n − 1)!
The body of the function starts by distinguishing the two cases, when n
is zero and when n is positive.
There seems to be a problem. Recall from Section 5.5 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.
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:
# 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,
consider this recursive definition of a function to add “just one more”
to a recursive invocation of itself:
FUNCTIONS 71
The definition works just fine, but any attempt to use it fails impres-
sively:
# just_one_more 42 ;;
Stack overflow during evaluation (looping recursion?).
1. an odd integer;
2. an integer less than or equal to 5;
3. the integer 0;
4. the truth value true.
Exercise 34
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
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.
74 PROGRAMMING WELL
# 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:8 8
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.
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 35
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 36
Construct a value for each of the following types.
1. bool * bool
Exercise 37
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 quotient 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 38
In Exercise 31, 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 37 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 79
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.
80 PROGRAMMING WELL
Exercise 39
Define an analogous function snd : int * int -> int that extracts the second
element of an integer pair. For instance,
# snd (3, 4) ;;
- : int = 4
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>
Exercise 40
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 C.4.2) for further
discussion.
Exercise 41
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
# | 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 42 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
quite clear.
3. true :: [false]
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 C.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
allowed to be used more than once in a
(and avoid a warning message).10 pattern.
# let is_empty (lst : int list) : bool =
# match lst with
# | [] -> true
# | _ :: _ -> false ;;
val is_empty : int list -> bool = <fun>
86 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 43
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) -> ...
88 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 44
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 89
# sum [1; 2; 4; 8] ;;
- : int = 15
What should this function return when applied to the empty list?
Exercise 45
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 46
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 47
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 48
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 49
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 50
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 91
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 ;;
- : int = 1903
# fullname ac ;;
- : string = "Alonzo Church"
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), 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.
In fact, map can even be used to define the functions inc_all and
square_all.
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 97
demonstrating that all along, power was a function (defined with fun)
of one argument (now called arg).
How about this definition of power?
# let rec power n x =
# if n = 0 then 1
98 PROGRAMMING WELL
# else x * power (n - 1) x ;;
val power : int -> int -> int = <fun>
# power 3 4 ;;
- : int = 64
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.
# square 4 ;;
- : int = 16
Exercise 51
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. Applying it only to its first argument
generates a function that applies that argument function to all of the
elements of a list. We can partially apply map to the successor function
to generate the inc_all function we had before.
# 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 52
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 44 and 45 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:
or, noting that + is itself the curried addition function we need as the
first argument to fold:
# let sum lst = fold (+) lst 0 ;;
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.
# let prod lst = fold ( * ) lst 1 ;;
val prod : int list -> int = <fun>
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 53
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 54
Define the length function that returns the length of a list, using fold_left.
Exercise 55
A cousin of the fold_left function is the function reduce,4 which is like fold_left 4
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.)
# | [] -> []
# | hd :: tl -> if hd > 0 then hd :: positives tl
# else positives tl ;;
val positives : int list -> int list = <fun>
Exercise 56
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 57
Provide definitions of evens, odds, positives, and negatives in terms of filter.
Exercise 58
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 59
Define a function append : int list -> int list -> int list (as described in
Exercise 49) 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. The result is a set of higher-order functions –
map, fold_left, fold_right, and filter – that are broadly useful.
Determining the best place to carve up code into separate factors to
take advantage of the commonalities and maximizing the utility of the
factors is an important skill, the basis for R E F A C T O R I N G of code, 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.
104 PROGRAMMING WELL
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.
106 PROGRAMMING WELL
9.1 Polymorphism
# let succ x = x + 1 ;;
val succ : int -> int = <fun>
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 107
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.
Propagating type information in this way results in a fully instan-
tiated type int -> int for the succ function. But what if there aren’t
enough constraints in the code to yield a fully instantiated type? The
I D E N T I T Y F U N C T I O N id, which just returns its argument unchanged,
is an example:
# 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>
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
The type variables make clear the intended constraints among f, xs,
and the return value map f xs.
Problem 60
For each of the following types construct an expression for which OCaml would infer
that type. For example, for the type bool * bool, the expression true, true would be
a possible answer. (The idea in this exercise is not that the expressions be practical or
do anything useful; they need only have the requested type. But no cheating by using
explicit typing annotations with the : operator!)
Exercise 61
Define polymorphic versions of fold and filter, providing explicit polymorphic typing
information.
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,
110 PROGRAMMING WELL
instead taking advantage of code that someone else has already writ-
ten. 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 functionality. The List module in particular
provides exactly the higher-order list processing functions presented
in this and the previous chapter as polymorphic functions. The docu-
mentation for the List module gives typings and descriptions for lots
of useful list processing functions. For instance, the module provides
the map, fold, and filter abstractions of Chapter 8, described in the
documentation as
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
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
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.
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]]
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:
As an example, we’ll use the (partial) credit card number from the
card in Figure 9.2:
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 .
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 113
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,
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.
Exercise 74
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.
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?
118 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 75
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 76
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 77
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 78
The variance of a sequence of n numbers x 1 , . . . , x n is given by the following equation:
Pn
(x − m)2
i =1 i
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.
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".
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 .
HANDLING ANOMALOUS CONDITIONS 125
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
# 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 79
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.
HANDLING ANOMALOUS CONDITIONS 129
Better yet is to make explicit the patterns that fall under the wildcard
allowing them to move up in the ordering.
# let rec zip_opt (xs : 'a list)
# (ys : 'b list)
# : ('a * 'b) list option =
# match xs, ys with
# | [], [] -> Some []
# | [], _
# | _, [] -> None
# | xhd :: xtl, yhd :: ytl ->
# match zip_opt xtl ytl with
# | None -> None
# | Some ztl -> Some ((xhd, yhd) :: ztl) ;;
val zip_opt : 'a list -> 'b list -> ('a * 'b) list option = <fun>
Exercise 80
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.
# let rec zip (xs : 'a list)
# (ys : 'b list)
# : ('a * 'b) list =
# match xs, ys with
# | [], [] -> []
# | [], _
# | _, [] -> raise (Invalid_argument
# "zip: unequal length lists")
# | xhd :: xtl, yhd :: ytl ->
# (xhd, yhd) :: (zip xtl ytl) ;;
val zip : 'a list -> 'b list -> ('a * 'b) list = <fun>
Exercise 81
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.
# zip_safe [1; 2; 3] [3; 2; 1] ;;
- : (int * int) list = [(1, 3); (2, 2); (3, 1)]
# zip_safe [1; 2; 3] [3; 2] ;;
- : (int * int) list = []
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 82
In Section 6.6, 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 83
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 84
As in Problem 60, 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 131
Problem 85
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 86
Provide a more succinct definition of the function f from Problem 85(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.
Anomalous conditions when running code cover a range of cases.
One class of anomalies are conditions that should never occur, follow-
ing from true bugs in code. For instance, when a function is applied to
a set of arguments for which it was explicitly not defined – for example,
applying the median function to an empty list, where the implementer
of the median function has specified that it is not defined in that case –
this constitutes a bug. The programmer who used the median function
in that way has made a mistake. Unfortunately, the bug appears only
at run time, when it is “too late”. The best we can do in such cases is
to abort the computation, returning control to some higher level for
which recovery from the bug is possible (if such a higher level even
exists), and providing as much information about the bug as possible.
Some programming languages provide specific tools for such cases. In
OCaml, exceptions are the right tool, raising an informative exception
and hoping that a higher level can recover. And proper programming
practice indicates doing just that.
For cases that are not simply bugs of this sort, that is, cases that
are anomalous from the usual course yet expected to be handled, the
choice between options and exceptions is governed by the properties
of the two approaches.
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
132 PROGRAMMING WELL
# 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
134 PROGRAMMING WELL
# 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";
# unit_test (try let _ = nth [1; 2; 3] 4 in
# false
# with
# | Failure _ -> true
# | _ -> false) "nth index too big";;
val nth_test : unit -> unit = <fun>
# nth_test () ;;
nth singleton passed
nth start passed
nth middle passed
nth last passed
nth index too big passed
- : unit = ()
We’ll later see more elegant ways to put together unit tests (Sec-
tion 17.6).
Exercise 87
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 []
〈〉 :: 〈〉
[ 〈〉 ; 〈〉 ; ...]
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
140 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 141
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 〈var〉
A L G E B R A I C D ATA T Y P E S 143
for just such cases. Such a pattern both pattern matches against the
〈pattern〉 as well as binding the 〈var〉 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 components. (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.)
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’:
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’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 145
Exercise 88
What do you expect the type of |> is?
Exercise 89
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 }
148 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 91
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 92
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
150 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 93
Define a version of eval that implements a different semantics for the expression
language, for instance, by rounding rather than truncating integer divisions.
Exercise 94
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)" (a)
The opposite process, recovering abstract syntax from concrete syntax, is called parsing.
More on this in the final project (Chapter A ).
Trees are a class of data structures that store values of a certain type
in a hierarchically structured manner. They constitute a fundamental
data structure, second only perhaps to lists in their repurposing flexi-
bility. Indeed, the arithmetic expressions of Section 11.4 are a kind of
tree structure, as are the binary search trees mentioned in Section ??.
In this section, we concentrate on a certain kind of polymorphic
(b)
B I N A RY T R E E , a kind of tree whose nodes have distinct left and right
Figure 11.3: Two trees: (a) an integer
subtrees, possibly empty. Some examples can be seen in Figure 11.3.
tree, and (b) a string tree.
A binary tree can be an empty tree (depicted with a bullet symbol (•)
152 PROGRAMMING WELL
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 95
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 96
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]
can abstract this tree walk functionality with a function that takes three
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 97
What is the appropriate type for the function foldbt just described?
Exercise 98
Define the function foldbt just described.
Exercise 99
Redefine the function sum_bintree using foldbt.
Exercise 100
Redefine the function preorder using foldbt.
Exercise 101
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
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.
158 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 102
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
IntQueue.empty_queue or IntQueue.enqueue. Let’s use this mod-
ule to perform various queue operations:
160 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]
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.
module 〈modulename〉 : 〈signature〉 =
〈moduledefinition〉
# 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
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 163
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.),
164 PROGRAMMING WELL
# PointOrderedType.norm ;;
- : float * float -> float = <fun>
# PointOrderedType.norm (1., 1.) ;;
- : float = 2.
# PointOrderedType.norm ;;
Line 1, characters 0-21:
1 | PointOrderedType.norm ;;
^^^^^^^^^^^^^^^^^^^^^
Error: Unbound value PointOrderedType.norm
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.
# open Queue ;;
# let intq = empty_queue
# |> enqueue 1
# |> enqueue 2 ;;
val intq : int Queue.queue = <abstr>
# let boolq = empty_queue
# |> enqueue true
166 PROGRAMMING WELL
Exercise 103
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:
# let rec eval (q : query)
# (idx : index)
# : string list =
# match q with
# | Word word ->
# let (_key, targets) =
# List.find (fun (w, _lst) -> w = word) idx
# in targets
# | And (q1, q2) ->
# intersection (eval q1 idx) (eval q2 idx)
# | Or (q1, q2) ->
# (eval q1 idx) @ (eval q2 idx) ;;
Line 10, characters 0-12:
10 | intersection (eval q1 idx) (eval q2 idx)
^^^^^^^^^^^^
Error: Unbound value intersection
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:
# let rec intersection set1 set2 =
# match set1, set2 with
# | [], _
# | _, [] -> []
# | h1 :: t1, h2 :: t2 ->
# if h1 = h2 then h1 :: intersection t1 t2
# else if h1 < h2 then intersection t1 set2
# else intersection set1 t2 ;;
val intersection : 'a list -> 'a list -> 'a list = <fun>
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.
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"
# |> add "c" ;; 6
You’ll notice that we don’t bother
val s : StringSet.set = <abstr>
adding types to the definitions of the
values in this module implementation.
Other operations work as well. Since the signature already provided
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
- : bool = false to provide further typing information in
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
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
intersection. We redefine the StringSet module accordingly, still
satisfying the same STRING_SET signature.
# (* Implementation of STRING_SET as list of strings.
# Assumes list is *sorted* with no duplicates. *)
# module StringSet : STRING_SET =
# struct
# type set = string list
172 PROGRAMMING WELL
# let empty = []
# let is_empty s = (s = [])
# let rec member elt s =
# match s with
# | [] -> false
# | hd :: tl -> if elt = hd then true
# else if elt < hd then false
# else member elt tl
# let rec add elt s =
# match s with
# | [] -> [elt]
# | hd :: tl -> if elt < hd then elt :: s
# else if elt = hd then s
# else hd :: add elt tl
# let union = List.fold_right add
# let rec intersection set1 set2 =
# match set1, set2 with
# | [], _ -> []
# | _, [] -> []
# | h1::t1, h2::t2 ->
# if h1 = h2 then h1 :: intersection t1 t2
# else if h1 < h2 then intersection t1 set2
# else intersection set1 t2
# end ;;
module StringSet : STRING_SET
# 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.
# type set
# (* The empty set *)
# val empty : set
# (* Returns true if set is empty; false otherwise *)
# val is_empty : set -> bool
# (* Adds integer to existing set (if not already a member) *)
# val add : int -> set -> set
# (* Union of two sets *)
# val union : set -> set -> set
# (* Intersection of two sets *)
# val intersection : set -> set -> set
# (* Returns true iff integer is in set *)
# val member: int -> set -> bool
# end ;;
module type INT_SET =
sig
type set
val empty : set
val is_empty : set -> bool
val add : int -> set -> set
val union : set -> set -> set
val intersection : set -> set -> set
val member : int -> set -> bool
end
type set
type element
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
^^^
Error: This expression has type string but an expression was
expected of type
StringSet.element
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
val empty : set sharing constraint, but multiple con-
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.
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
# | -1 (* less *) -> intersection t1 set2
# | _ (* greater *) -> intersection set1 t2)
# end ;;
module MakeOrderedSet :
functor (Elements : ORDERED_TYPE) ->
sig
type set
type element = Elements.t
val empty : set
val is_empty : set -> bool
180 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).
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
# module StringStringSetDictArg
# : (DICT_ARG with type key = string
# and type value = StringSet.set) =
# struct
# type key = string
# type value = StringSet.set
# type order = Less | Equal | Greater
# let compare x y = if x < y then Less
# else if x = y then Equal
# else Greater
# end ;;
module StringStringSetDictArg :
sig
type key = string
type value = StringSet.set
type order = Less | Equal | Greater
val compare : key -> key -> order
end
# match q with
# | Word word -> (match Index.lookup idx word with
# | None -> StringSet.empty
# | Some v -> v)
# | And (q1, q2) -> StringSet.intersection (eval q1 idx)
# (eval q2 idx)
# | Or (q1, q2) -> StringSet.union (eval q1 idx)
# (eval q2 idx) ;;
val eval : query -> Index.dict -> StringSet.set = <fun>
useful on occasions where the signature is quite short and will only be
used once, so retaining a name for it isn’t needed.
There is a third method, widely used within OCaml’s own imple-
mentation of library modules. All of the components defined in a .ml
file automatically constitute a module, whose name is generated by
converting the first letter of the filename to uppercase. For example, if
we have a file named queue.ml whose contents is
type 'a queue = 'a list
let empty_queue : 'a queue = []
186 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
| Less -> false
| Greater -> member elt tl
| hd :: tl ->
let open Elements in
match compare elt hd with
| Less -> elt :: s
| Equal -> s
| Greater -> hd :: add elt tl
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
does provide these and many other data structures – as L I B R A RY M O D -
U L E S.
In particular, the Set library module provides functionality much
like the Set module in the previous section, and the Map library mod-
ule provides functionality much like our dictionary module and its
MakeOrderedDict functor.
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 189
We define here a signature for modules that deal with images and their
manipulation.
module type IMAGING =
sig
(* types for images, which are composed of pixels *)
type image
type pixel
(* an image size is a pair of ints giving number of
rows and columns *)
type size = int * int
(* converting between integers and pixels *)
val to_pixel : int -> pixel
val from_pixel : pixel -> int
(* apply an image filter, a function over pixels,
to every pixel in an image *)
val filter : (pixel -> pixel) -> image -> image
(* apply an image filter to two images, combining
the images pixel by pixel *)
val filter2 : (pixel -> pixel -> pixel)
-> image -> image -> image
(* return a "constant" image of the specified size
where every pixel has the same value *)
val const : pixel -> size -> image
(* display the image in a graphics window *)
val depict : image -> unit
end ;;
The pixels that make up an image are specified by the following signa-
ture:
module type PIXEL =
sig
type t
val to_pixel : int -> t
val from_pixel : t -> int
end
Problem 104
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....
190 PROGRAMMING WELL
Problem 105
Write code that uses the IntPixel module to define an imaging module called
IntImaging.
Problem 106
Write code to use the IntImaging module that you defined in Problem 105 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 109
Now use the functor MakeInterval to define a module DiscreteTimeInterval
that provides interval functionality over discrete times as defined by the module
DiscreteTime above.
Problem 110
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 111
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.4.
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
192 PROGRAMMING WELL
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 112
Define a weight type consisting of a single floating point weight.
Problem 113
Define a shape type, a variant type that allows for three different shapes: circles, ovals,
and fins.
Problem 114
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,
the types to use for leaft and nodet. The following module signature
will work:
module type BINTREE_ARG =
sig
type leaft
type nodet
end ;;
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 193
Problem 115
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 119
What is the type of size?
Problem 120
Use the fact that the walk function is curried to give a slightly more concise definition for
size.
Problem 121
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.
194 PROGRAMMING WELL
• Lab 8: Functors
Exercise 124
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
198 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 199
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.” The two subderivations for
P ⇓ m and Q ⇓ n are derived independently, and not in any particular
order.
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⇓
¯
¯ 3 + 5⇓
¯ ¯
¯
¯ ¯ 3⇓3
¯ ¯
¯
¯
¯ ¯ 5⇓5
⇓8
¯
¯
¯
¯ 7⇓7
⇓ 15
200 PROGRAMMING WELL
Exercise 125
Why is the proof for the value of 3 + 5 + 7 not structured as
3 + 5 + 7⇓
¯
¯ 3⇓3
¯
¯ 5 + 7⇓
¯ ¯
¯
¯ ¯ 5⇓5
¯ ¯
¯ ¯ 7⇓7
¯
¯ ⇓ 12
⇓ 15 ?
P / Q⇓
3
¯ What may be mind-boggling here is
¯ P ⇓m
the role of the mathematical notation
(R / )
¯
used in the result part of the rule. How
¯
¯ Q ⇓n
is it that we can make use of notations
⇓ ⌊m/n⌋ like ⌊m/n⌋ in defining the semantics of
the / operator? Doesn’t appeal to that
In this rule, we’ve used some standard mathematical notation in the kind of mathematical notation beg the
question? Or at least call for its own
final result: / for numeric division and ⌊ ⌋ for truncating a real number
semantics? Yes, it does, but since we
to an integer. have to write down the semantics of
These rules for addition and division may look trivial, but they are constructs somehow or other, we use
commonly accepted mathematical
not. The division rule specifies that the / operator in OCaml when notation applied in the context of
applied to two numerals specifies the integer portion of their ratio. The natural language (in the case at hand,
English). You may think that this merely
language being specified might have been otherwise.3 The language
postpones the problem of giving OCaml
might have used a different operator (like //) for integer division, semantics by reducing it to the problem
of giving semantics for mathematical
P // Q ⇓ notation and English. You would
¯
¯ P ⇓m be right, and the problem is further
¯ exacerbated when the semantics makes
¯
¯ Q ⇓n use of mathematical notation that is not
so familiar, for instance, the substitution
⇓ ⌊m/n⌋ notation to be introduced shortly. But
we have to start somewhere.
(as happens to be used in Python 3 for instance). The example should
make clear the distinction between the O B J E C T L A N G UA G E whose
semantics is being defined and the M E TA L A N G UA G E being used to
define it.
Similarly, the rule could have defined the result differently, say
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
SEMANTICS: THE SUBSTITUTION MODEL 201
Exercise 126
Write evaluation rules for the other binary operators and the unary operators you added
in Exercise 124.
let x = D in B ⇓
¯
¯ D⇓v
D
(R let )
¯
¯
¯ B [x 7→ v D ] ⇓ v B
⇓ vB
(x * x)[x 7→ 5] = 5 * 5
let x = 5 in x * x ⇓ 25
202 PROGRAMMING WELL
let x = 5 in x * x ⇓
¯
¯ 5⇓5
¯
¯ 5 * 5 ⇓¯
¯
¯ ¯ 5⇓5
¯ ¯
¯ ¯
¯
¯ ¯ 5⇓5
¯
¯ ⇓ 25
⇓ 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 ⇓
¯
¯ 5⇓
¯ ¯
¯ ¯ .
¯ ¯ .
¯
¯ .
¯ ⇓ ···
¯
¯
¯ ···
⇓ ···
This subderivation can be completed using the R int rule, which re-
quires no subderivations itself.
let x = 5 in x * x ⇓
¯
¯ 5⇓
¯
¯
¯ |
¯
¯ ⇓5
¯
¯
¯ ···
⇓ ···
Now
B [x 7→ v D ] = (x * x)[x 7→ 5]
= x[x 7→ 5] * x[x 7→ 5]
=5 * 5
let x = 5 in x * x ⇓
¯
¯ 5⇓5
¯
¯
¯ 5 * 5⇓
¯
¯ ..
¯ ¯
¯
¯ ¯ .
¯
¯ ⇓ ···
⇓ ···
let x = 5 in x * x ⇓
¯
¯ 5⇓5
¯
¯ 5 * 5 ⇓¯
¯
¯ ¯ 5⇓m
¯ ¯
¯ ¯
¯
¯ ¯ 5⇓n
¯
¯ ⇓ m ·n
⇓ ···
let x = 5 in x * x ⇓
¯
¯ 5⇓5
¯
¯ 5 * 5 ⇓¯
¯
¯ ¯ 5⇓5
¯ ¯
¯ ¯
¯
¯ ¯ 5⇓5
¯
¯ ⇓ 25
⇓ 25
Exercise 127
Carry out derivations for the following expressions:
1. let x = 3 in let y = 5 in x * y
204 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?
Exercise 128
Verify using this definition for substitution the derivation above showing that
(x * x)[x 7→ 5] = 5 * 5.
You may have noticed in Exercise 127 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
⇓
¯
¯ 3⇓3
¯
¯ let x = 5 in 3 ⇓¯
¯
¯ ¯ 5⇓5
¯ ¯
¯ ¯
¯
¯ ¯ 3⇓3
¯
¯ ⇓3
⇓3
(let x = 5 in x)[x 7→ 3] .
(let x = 5 in x)[x 7→ 3]
Exercise 129
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
206 PROGRAMMING WELL
3. let x = 3 in x
4. let f = f 3 in x + y
5. (fun x -> x + x) x
Exercise 130
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 131
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 130(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 207
m[x 7→ Q] = m
x[x 7→ Q] = Q
y[x 7→ Q] = y where x ̸≡ y
(P + R )[x 7→ Q] = P [x 7→ Q] + R[x 7→ Q] and similarly for other binary operators
(let y = D in B )[x 7→ Q] = let y = D[x 7→ Q] in B [x 7→ Q] where x ̸≡ y
(let x = D in B )[x 7→ Q] = let x = D[x 7→ Q] in B
Exercise 132
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 7→ 3]
2. (x + x)[y 7→ 3]
3. (x * x)[x 7→ 3 + 4]
4. (let x = y in y + x)[y 7→ z]
5. (let x = y in y + x)[x 7→ z]
Exercise 133
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 134
Augment the type definitions to allow for other binary operations (subtraction and
multiplication, say) and for unary operations (negation).
Exercise 135
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 209
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) -> ...
210 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 136
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 137
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⇓
¯
¯ P ⇓ fun x -> B
¯
¯ Q ⇓ vQ (R app )
¯
¯
¯ B [x →
7 vQ ] ⇓ v B
⇓ vB
Exercise 138
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)
⇓
¯
¯ (fun x -> x + x) ⇓ (fun x -> x + x)
¯
¯ 3 * 4 ⇓¯
¯
¯ ¯ 3⇓3
¯ ¯
¯ ¯
¯
¯ ¯ 4⇓4
¯
¯
¯ ⇓ 12
¯ 12 + 12 ⇓
¯
¯ ¯
¯ ¯ 12 ⇓ 12
¯ ¯
¯
¯ 12 ⇓ 12
¯
¯
¯
¯ ⇓ 24
⇓ 24
Exercise 139
1. (fun x -> x + 2) 3
following derivation:
⇓
¯
¯ fun z -> y ⇓ fun z -> y
¯
¯ (fun y -> (fun z -> y) 3) 1
¯
¯
¯
¯ ⇓
¯ ¯
¯ ¯ (fun y -> (fun z > y) 3) ⇓ (fun y -> (fun z -> y) 3)
¯ ¯
¯
¯
¯ ¯ 1⇓1
¯ ¯
¯ ¯ (fun z -> 1) 3 ⇓
¯ ¯ ¯
¯ ¯ ¯ fun z -> 1 ⇓ fun z -> 1
¯ ¯ ¯
¯ ¯ ¯
¯ ¯
¯ ¯ 1⇓1
¯ ¯
⇓1
¯ ¯
¯
¯
¯ ⇓1
⇓1
Exercise 140
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 141
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 7→ P ] = · · ·
Try to work out your answer before checking it with the full definition of substitution in
Figure 13.4.
216 PROGRAMMING WELL
Exercise 142
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 7→ 3]
4. (let x = y * y in x + x)[y 7→ 3]
Exercise 143
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 144
Revise the definition of subst to eliminate the problem of variable capture by imple-
menting the set of rules given in Figure 13.4.
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 217
m[x 7→ P ] = m (13.7)
x[x 7→ P ] = P (13.8)
y[x 7→ P ] = y where x ̸≡ y (13.9)
(Q + R )[x 7→ P ] = Q[x 7→ P ] + R[x 7→ P ] (13.10)
and similarly for other binary operators
Q R[x 7→ P ] = Q[x 7→ P ] R[x 7→ P ] (13.11)
(fun x -> Q )[x 7→ P ] = fun x -> Q (13.12)
(fun y -> Q )[x 7→ P ] = fun y -> Q[x 7→ P ] (13.13)
where x ̸≡ y and y ̸∈ F V (P )
(fun y -> Q )[x 7→ P ] = fun z -> Q[y 7→ z][x 7→ P ] (13.14)
where x ̸≡ y and y ∈ F V (P ) and z is a fresh variable
(let x = Q in R )[x 7→ P ] = let x = Q[x 7→ P ] in R (13.15)
(let y = Q in R )[x 7→ P ] = let y = Q[x 7→ P ] in R[x 7→ P ] (13.16)
where x ̸≡ y and y ̸∈ F V (P )
(let y = Q in R )[x 7→ P ] = let z = Q[x 7→ P ] in R[y 7→ z][x 7→ P ] (13.17)
where x ̸≡ y and y ∈ F V (P ) and z is a fresh variable
ceeds as follows:
let rec x = D in B ⇓
¯
¯ D⇓v
¯ D
¯
¯ B [x 7→ v D [x 7→ 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 145
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⇓
¯
¯ P ⇓ fun x -> B
¯
¯ Q ⇓ vQ (R app )
¯
¯
¯ B [x →
7 vQ ] ⇓ v B
⇓ vB
let x = D in B ⇓
¯
¯ D⇓v
D
(R let )
¯
¯
¯ B [x 7→ v D ] ⇓ v B
⇓ vB
let rec x = D in B ⇓
¯
¯ D⇓v
¯ D
¯
¯ B [x 7→ v D [x 7→ let rec x = v D in x]] ⇓ v B
⇓ vB
(R letrec )
SEMANTICS: THE SUBSTITUTION MODEL 221
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 225
# match xs with
# | [] -> []
# | hd :: tl -> insert lt (sort lt tl) hd
# end ;;
module InsertSort : SORT
Exercise 146
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 227
Ti s (n) = a · n 2 + b
whereas for mergesort, the time required to sort the list grows as the
function
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 B.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 231
λn.n 2 ∈ O(λn.10n 2 + 3) .
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 147
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 233
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
234 PROGRAMMING WELL
2n ≫ n k
3n ≫ 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)
236 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)
away all of the constants and lower order terms to get at the essence of
the growth rate.
Problem 149
Recall that the Stdlib.compare function compares two values, returning an int based
on their relative magnitude: compare x y returns 0 if x is equal to y, -1 if x is less than y, 7
For reference, this built-in length
and +1 if x is greater than y.
function is, unsurprisingly, linear in the
A function compare_lengths : ’a list -> ’b list -> int that compares
length of its argument.
the lengths of two lists can be implemented using compare by taking advantage of the
length function7 from the List module:
let compare_lengths xs ys =
compare (List.length xs) (List.length ys) ;;
For instance,
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.
Trevapp (0, m) = c
Trevapp (n + 1, m) = k + Trevapp (n, m + 1)
240 PROGRAMMING WELL
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 some
further constant overhead (k 1 ). Since we don’t know which way the
computation will branch, we have to make the worst-case assump-
tion: whichever is bigger. Which of the two is bigger depends on the
constants, but we can be sure, in any case, that the time required is
certainly less than the sum of the two.
Tisort (0) = c
Tisort (n + 1) = k + Tisort (n) + Tinsert (n)
242 PROGRAMMING WELL
∈ 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.
Exercise 150
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)
= ···
= k · log n + c · n
∈ O(n)
T (n) = k · n + 2 · T (n/2)
= k · n · log n + c · n
∈ O(n log n)
n 2 ≫ n log n
Recall the Luhn check algorithm from Section 9.6, and its various
component functions: evens, odds, doublemod9, sum.
Problem 151
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 152
What is the time complexity of the odds function from Problem 69 (in big-O notation)?
Problem 153
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 )
• f ∈ O(n 2 /c) for all c > 0
• f ∈ O(c · n 2 ) for all c > 0
• f ∈ O(2n )
• f ∈ O(2n /c) for all c > 0
• f ∈ O(c · 2n ) for all c > 0
Problem 154
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.
# 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 157
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
258 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.
# mlength r ;;
- : int = 0
# mlength s ;;
- : int = 1
# mlength t ;;
- : int = 2
Figure 15.3: Pictorial representation
of (top) the state of memory after
Box and arrow diagrams (Figure 15.3) help in figuring out what’s
building some mutable list structures,
going on here. and (bottom) updating with r := !t.
The nil has become garbage and the
Exercise 159 mutable lists r, s, and t now have cycles
Write functions mhead and mtail that extract the head and the (dereferenced) tail from a in them.
mutable list. For example,
260 PROGRAMMING WELL
# mhead t ;;
- : int = 2
# mtail t ;;
- : int mlist = {contents = Cons (1, {contents = Nil})}
Because the lists are mutable, we can modify the tail of s (that is, r)
to point to t.
# r := !t ;;
- : unit = ()
# mlength t ;;
Stack overflow during evaluation (looping recursion?).
Problem 161
Provide an implementation of the mlength function that handles cyclic lists, so that
# mlength t ;;
- : int = 3
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 162
Define a function mfirst : int -> ’a mlist -> ’a list that returns a list (im-
mutable) of the first n elements of a mutable list:
Problem 163
Write code to define a mutable integer list alternating such that for all integers n, the
expression mfirst n alternating returns a list of alternating 1s and 2s, for example,
# mfirst 5 alternating ;;
- : int list = [1; 2; 1; 2; 1]
# mfirst 8 alternating ;;
- : int list = [1; 2; 1; 2; 1; 2; 1; 2]
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
262 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.
Exercise 164
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.
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
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.
# dequeue q ;;
- : int option = Some 1
(* 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 ;;
# 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 -> unit
# (* Removes the key from the dictionary. If the key is
# not present, leaves the original dictionary unchanged. *)
# val remove : dict -> key -> unit
# end ;;
module type MDICT =
sig
type key
type value
type dict
val empty : unit -> dict
val lookup : dict -> key -> value option
val member : dict -> key -> bool
val insert : dict -> key -> value -> unit
val remove : dict -> key -> unit
end
a functor.
# module type MDICT_ARG =
# sig
# (* Types to be used for the dictionary keys and values *)
# type key
# type value
# (* size -- Number of elements that can be stored in the
# dictionary *)
# val size : int
# (* hash key -- Returns the hash value for a key. *)
# val hash_fn : key -> int
# end ;;
module type MDICT_ARG =
sig type key type value val size : int val hash_fn : key -> int
end
sig
type key = int
type value = string
type dict
val empty : unit -> dict
val lookup : dict -> key -> value option
val member : dict -> key -> bool
val insert : dict -> key -> value -> unit
val remove : dict -> key -> unit
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 165
Complete the implementation by providing implementations of the remaining func-
tions lookup, member, insert, and remove.
Exercise 166
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 167
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
270 PROGRAMMING WELL
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 loops:
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:
LOOPS AND PROCEDURAL PROGRAMMING 273
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
the stack frames, to calculate the final answer. Figure 16.1 depicts this
linearly growing stack of suspended calls. length [1; 2; 3]
The iterative approach, on the other hand, needs no stack of sus- ⇒ 1 + length [2; 3]
pended computations. The single call to length_iter invokes the ⇒ 1 + ( 1 + length [3] )
while loop to iteratively increment the counter and drop elements
⇒ 1 + ( 1 + ( 1 + length [] ))
from the list. The computation is “flat”.
⇒ 1 + ( 1 + ( 1 + 0))
The difference can be seen forcefully when computing the length of ⇒ 1 + ( 1 + 1)
a very long list. Here, we’ve defined very_long_list to be a list with ⇒ 1 + 2
one million elements. ⇒ 3
# 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.
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.”
Another advantage of procedural programming is the ability to avoid
building of new data structures. Think of the map function over lists,
which can be implemented as follows:
276 PROGRAMMING WELL
# 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 168
Implement the module Metered.
Problem 169
Reimplement the zip function of Section 10.3.2 using metered conses and pairs.
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 170
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 171
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.
P Q⇓
¯
¯ P ⇓ fun x -> B
¯
¯ Q ⇓ vQ (R app )
¯
¯
¯ B [x →
7 vQ ] ⇓ v B
⇓ vB
let x = D in B ⇓
¯
¯ D⇓v
D
(R let )
¯
¯
¯ B [x 7→ v D ] ⇓ v B
⇓ vB
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
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?
Of course, that doesn’t work, because the name ones isn’t itself avail-
able in the definition. That requires a let rec.
# head ones ;;
- : int = 1
# head (tail ones) ;;
- : int = 1
# head (tail (tail ones)) ;;
- : int = 1
288 PROGRAMMING WELL
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
M E M O I Z AT I O N .2 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:
294 PROGRAMMING WELL
Exercise 172
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:
π 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 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:
296 PROGRAMMING WELL
We can now search for a value accurate to within any number of digits
we desire:
# within 0.01 pi_approximations ;;
- : float = 3.13659268483881615
# within 0.001 pi_approximations ;;
- : float = 3.14109265362104129
Exercise 173
As mentioned in Exercise 33, 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.
Exercise 174
Define a value falses to be an infinite stream of the boolean value false.
Exercise 175
What is the type of falses?
Exercise 176
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 177
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 178
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 179
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.
Of course, tests can pass or fail, which we represent by an expres-
sion that returns either true or false respectively. But tests can have
298 PROGRAMMING WELL
other outcomes as well; there are other forms of failing than returning
false. In particular, a test might raise an exception, or it might not
terminate at all. In order to deal with tests that might not terminate,
we’ll need a way of safely running these tests in a context in which we
cut off computation after a specified amount of time. The computation
will be said to have T I M E D O U T . To record the outcome of a test, we’ll
define a variant type:
# 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 ;;
# try
# if timeout time condition
# then continue label Passed
# else continue label Failed
# with
# | Timeout -> continue label (Timed_out time)
# | exn -> continue label
# (Raised_exn (Printexc.to_string exn)) ;;
val run_test : test -> (string -> status -> unit) -> unit = <fun>
# let tests =
# [ test "should fail" (lazy (3 > 4));
# test "should pass" (lazy (4 > 3));
# test "should time out" (lazy (let rec f x = f x in f 1));
# test "should raise exception" (lazy ((List.nth [0; 1] 3) = 3))
# ] ;;
val tests : test list =
[{label = "should fail"; condition = <lazy>; time = 5};
{label = "should pass"; condition = <lazy>; time = 5};
{label = "should time out"; condition = <lazy>; time = 5};
{label = "should raise exception"; condition = <lazy>; time =
5}]
# 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”:
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.
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”
304 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 305
# | 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};
306 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);
310 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 311
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
314 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 180
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 draws a list information for objects. The elements
of scene are instances of various
of 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 317
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.
320 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 182
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 183
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 184
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).
The semantics for this language was provided through the apparatus of
evaluation rules, which defined derivations for judgements of the form
P ⇓v
322 PROGRAMMING WELL
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⇓v
D
(R let )
¯
¯
¯ B [x 7→ v D ] ⇓ v B
⇓ vB
E ⊢ let x = D in B ⇓
¯
¯ E ⊢D ⇓v
D
(R let )
¯
¯
¯ E {x 7→ v D } ⊢ B ⇓ v B
⇓ vB
E ⊢ x ⇓ E (x) (R var )
{} ⊢ let x = 3 in x + x ⇓
¯
¯ {} ⊢ 3 ⇓ 3
¯
¯ {x →
7 3} ⊢ x + x ⇓¯
¯
¯ ¯ {x 7→ 3} ⊢ x ⇓ 3
¯ ¯
¯ ¯
¯
¯ ¯ {x 7→ 3} ⊢ x ⇓ 3
¯
¯ ⇓6
⇓6
Exercise 185
Construct the derivation for the expression
let x = 3 in
let y = 5 in
x + y ;;
SEMANTICS: THE ENVIRONMENT MODEL 325
Exercise 186
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 ⇓
¯
¯ E ⊢ P ⇓ fun x -> B
¯
¯ E ⊢ Q ⇓ vQ (R app )
¯
¯
¯ E {x →
7 vQ } ⊢ B ⇓ v B
⇓ vB
Exercise 187
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)
⇓
¯
¯ {} ⊢ (fun x -> x + x) ⇓ (fun x -> x + x)
¯
¯ {} ⊢ 3 * 4 ⇓¯
¯
¯ ¯ {} ⊢ 3 ⇓ 3
¯ ¯
¯ ¯
¯
¯ ¯ {} ⊢ 4 ⇓ 4
¯
¯
¯ ⇓ 12
¯ {x 7→ 12} ⊢ x + x ⇓
¯
¯ ¯
¯ ¯ {x 7→ 12} ⊢ x ⇓ 12
¯ ¯
¯
¯ {x 7→ 12} ⊢ x ⇓ 12
¯
¯
¯
¯ ⇓ 24
⇓ 24
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 ⇓v
D
(R let )
¯
¯
¯ E {x 7→ v D } ⊢ B ⇓ v B
⇓ vB
E ⊢P Q ⇓
¯
¯ E ⊢ P ⇓ fun x -> B
¯
¯ E ⊢ Q ⇓ vQ (R app )
¯
¯
¯ E {x →
7 vQ } ⊢ B ⇓ v B
⇓ vB
SEMANTICS: THE ENVIRONMENT MODEL 327
# 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
the derivation
Exercise 188
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-
SEMANTICS: THE ENVIRONMENT MODEL 329
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 ⇓v
D
(R letrec )
¯
¯
¯ E {x 7→ 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 ⇓
¯
¯ E d ⊢ P ⇓ [E l ⊢ fun x -> B ]
¯
¯ E d ⊢ Q ⇓ vQ (R app )
¯
¯
¯ E {x →
7 vQ } ⊢ B ⇓ v B
l
⇓ vB
Exercise 189
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 190
Carry out the derivation using the lexical environment semantics for the expression
(fun x -> fun y -> x + y) 1 2 ;;
Problem 191
In problem 156, you evaluated several expressions as OCaml would, with lexical scoping.
Which of those expressions would evaluate to a different value using dynamic scoping?
Exercise 192
Adjust the substitution semantics rules for booleans from Exercise 136 to construct
environment semantics rules for the constructs.
Exercise 193
Adjust the substitution semantics rules for conditional expressions (if 〈〉 then 〈〉 else 〈〉
) from Exercise 137 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 ⇓v
D
(R let )
¯
¯
¯ E {x 7→ v D } ⊢ B ⇓ v B
⇓ vB
Ed ⊢ P Q ⇓
¯
¯ E d ⊢ P ⇓ [E l ⊢ fun x -> B ]
¯
¯ E d ⊢ Q ⇓ vQ (R app )
¯
¯
¯ E {x →
7 vQ } ⊢ B ⇓ v B
l
⇓ 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)
P ⇓ vP , S′
will need to be relative to a store in addition to an environment, so
evaluation judgements will look like E , S ⊢ P ⇓ · · · . E , S ⊢ |{z}
Because the store can change as a side effect of evaluation (that’s
|{z} | {z }
the whole point of mutability), the result of evaluation can’t simply be a
a b c
value. We’ll need access to the modified store as well. So the right-hand
| {z }
side of the evaluation arrow ⇓ will provide both a value and a store. Our d
final form for evaluation judgements is thus
| {z }
e
Figure 19.3: Anatomy of an evaluation
E , S ⊢ P ⇓ vP , S′ . judgement. (a) The context of evalua-
tion, including an environment E and a
store S. (b) The expression to be evalu-
ated. (c) The result of the evaluation, a
(See Figure 19.3 for a breakdown of such a judgement.) value and a store mutated by side effect.
A semantic rule for references reflects these ideas: (d) The evaluation of P to its result.
(e) The judgement as a whole. “In the
environment E and store S, expression
E , S ⊢ ref P ⇓ P evaluates to value v P with modified
¯ store S ′ .”
¯ E , S ⊢ P ⇓ vP , S′ (R ref )
¯
Exercise 194
E , S ⊢ P := Q ⇓
¯
¯ E , S ⊢ P ⇓ l , S′
(R assign )
¯
¯ E , S ′ ⊢ Q ⇓ vQ , S ′′
¯
⇓ (), S ′′ {l 7→ 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 195
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 ⇓ v , S′
D
(R let )
¯
¯ E {x 7→ v D }, S ′ ⊢ B ⇓ v B , S ′′
¯
⇓ v B , S ′′
Ed , S ⊢ P Q ⇓
¯ E d , S ⊢ P ⇓ [E l ⊢ fun x -> B ], S ′
¯
¯
¯ E d , S ′ ⊢ Q ⇓ vQ , S ′′ (R app )
¯
¯
¯ E {x →
l 7 vQ }, S ′′ ⊢ B ⇓ v B , S ′′′
⇓ v B , S ′′′
340 PROGRAMMING WELL
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 7→ 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:
E , S ⊢ let rec x = D in B ⇓
¯
¯ E {x 7→ l }, S{l 7→ unassigned} ⊢ D[x 7→ !x] ⇓ v , S ′
¯ D
¯ E {x 7→ l }, S ′ {l 7→ v D } ⊢ B [x 7→ !x] ⇓ v B , S ′′
¯
⇓ v B , S ′′
(R letrec )
Problem 196
For the formally inclined, prove that the semantic rule for let rec above is equivalent to
the syntactic sugar approach.
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.
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. The logged start and end times indicate that the short and long
tasks required about 0.1 and 0.2 seconds, respectively, together requir-
ing 0.3 seconds, as expected.
If we’d like the two tasks 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. OCaml
provides for creating and manipulating threads in its Thread module.3 3
The Thread module is part of OCaml’s
threads library, which allows for creating
To fork a new thread, we use the Thread.create function, which takes
multiple concurrent threads. To make
a function and its argument and returns a separate new thread of com- use of the library in the R E P L , you’ll
putation (a value of type Thread.t) in which the function is applied to need to make it available with
#use "topfind" ;;
its argument. Its type is thus (’a -> ’b) -> ’a -> Thread.t. So we
#thread ;;
can evaluate tasks A and B in separate threads, concurrently, as follows:
For instance, in the example above, how can taskA, isolated in its
own thread, inform the thread running taskB about its return value?
Similarly, how can taskB communicate information to taskA if it
needs to?
A simple mechanism for this interthread communication is for the
threads to share mutable values, which serve as channels of commu-
nication between the threads. Let’s start with how the created thread
executing taskA can communicate its return value to the main thread
that needs to calculate the sum.
We’ll define another test function called test_communication
to test the communication between two tasks executed in separate
threads as above.
...
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.
350 PROGRAMMING WELL
...
match !shared_result with
| Some resultA ->
(* compute with the two results *)
resultA + resultB
| None ->
(* Oops, taskA hasn't completed! *)
failwith "shouldn't happen!" ;;
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.1231 task_long : starts]
[2.1231 task_short: starts]
[2.2233 task_short: ends]
[2.3233 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.
In general, one doesn’t have the kind of detailed information about
run times of various tasks as we have for task_short and task_long.
This kind of concurrent computation, without careful controls, thus
leads to indeterminacy at runtime. And debugging these intermittent
bugs that can come and go, perhaps appearing only rarely, can be
especially confounding. More tools are needed.
The lesson here is that the main thread shouldn’t attempt to use the
shared variable until the forked thread has completed. We thus need
a way of guaranteeing that a thread has completed. One solution you
may have thought of is to have the main thread test if the shared value
has not been properly set, and if not, to just “try again later”. We can
implement this with a simple loop,
while !shared_result == None do
Thread.delay 0.01
done;
# done;
# match !shared_result with
# | Some resultA ->
# (* compute with the two results *)
# resultA + resultB
# | None ->
# (* Oops, taskA hasn't completed! *)
# failwith "shouldn't happen!" ;;
val test_communication : (unit -> int) -> (unit -> int) -> int =
<fun>
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.
Instead, we’d like to be able to directly specify to wait until
the forked thread completes. The companion to the fork function
Thread.create is the join function Thread.join. Thread.join
takes a thread as its argument and returns only once that thread has
completed. By requiring the join before accessing the shared variable,
we are guaranteed that the variable will have been updated at the time
that we need it.
# let test_communication taskA taskB =
# let shared_result = ref None in
# let thread =
# Thread.create
# (fun () -> shared_result := Some (taskA ())) () in
# let resultB = taskB () in
# Thread.join thread;
# match !shared_result with
# | Some resultA ->
# (* compute with the two results *)
# resultA + resultB
# | None ->
# (* Oops, taskA hasn't completed! *)
# failwith "shouldn't happen!" ;;
val test_communication : (unit -> int) -> (unit -> int) -> int =
<fun>
Using this version of the test, the race condition is avoided, and the
calculation completes properly.
# test_communication task_long task_short ;;
[4.5432 task_short: starts]
[4.5433 task_long : starts]
[4.6434 task_short: ends]
[4.7434 task_long : ends]
- : int = 3
20.5 Futures
Exercise 197
Exercise 97 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.
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
CONCURRENCY 355
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
356 PROGRAMMING WELL
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.
# 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. In particular, if task A
completes first, the two accounts should see withdrawals of $75 and $0
respectively, leaving a balance of $25, that is, the simulation function
should return the triple (75, 0, 25). If task B completes first, the two
accounts should see withdrawals of $0 and $50 respectively, leaving a
balance of $50, that is, the simulation function should return the triple
(0, 50, 50). Let’s try it.
# test_wds () ;;
- : int * int * int = (0, 50, 50)
into two parts: the computation of the updated balance and the up-
date of the balance variable itself:
let diff = balance - amt in balance <- diff
Doing so separates the reading 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 executes, immediately after the balance check,
and after computing the updated balance just before carrying out the
update. For this purpose, 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) ;;
val random_delay : float -> unit = <fun>
CONCURRENCY 357
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
358 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
···
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
summary, both attempted withdrawals succeed, dispensing both $75
and $50, leaving a surprising $50 balance. Sure enough, Figure 20.5
indicates that such outcomes were actually attested in the simulations.
Exercise 198
Construct an interleaving in which both withdrawals succeed, leaving a balance of $25.
Exercise 199
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
6
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.6 But if not,
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:
This idiom – wrapping a critical region with a lock at the beginning and
an unlock at the end – captures the stereotypical use of locks.
360 PROGRAMMING WELL
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;
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
be permanently prevented from carrying out their own computations
requiring the lock. We can codify the importance of matching the locks
and unlocks by way of an abstracted function that wraps a computa-
tion with the lock and its corresponding unlock. We call the function
with_lock:
Exercise 200
Define a version of with_lock that handles exceptions by making sure to unlock the
lock.
Using with_lock, the withdraw method becomes
amt
end else 0) ;;
valid? first second balance count
With this modified implementation of accounts, the simulation of ✓ 75 0 25 50
many trials of simultaneous deposits performs much better, with only ✓ 0 50 50 50
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.
A
Final project: Implementing MiniML
A .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 A .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 A .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 201
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 A .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 A .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 367
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 201, 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
368 PROGRAMMING WELL
Exercise 203
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 204
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 205
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 206
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 207
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 208
Implement the various functions involved in the Env module and test
them thoroughly.
(fun x -> x + x) 5
E ⊢P Q ⇓
¯
¯ E ⊢ P ⇓ fun x -> B
¯
¯ E ⊢ Q ⇓ vQ (R app )
¯
¯
¯ E {x →
7 vQ } ⊢ B ⇓ v B
⇓ vB
E ⊢ let x = D in B ⇓
¯
¯ E ⊢D ⇓v
D
(R let )
¯
¯
¯ E {x 7→ v D } ⊢ B ⇓ v B
⇓ vB
let x = 3 * 4 in x + 1 ;;
Stage 209
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 373
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.
374 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 A .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 210
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 375
Stage 211
(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 377
Stage 212
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
Problem sets
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.
The donation game is an instance of a classic game-theory thought
experiment called the P R I S O N E R ’ S D I L E M M A . A prisoner’s dilemma is
Player 2
a type of game involving two players in which each player is individ-
Cooperate Defect
ually incentivized to choose a particular action, even though it may
Cooperate (3, 3) (−2, 5)
not result in the best global outcome for both players. The outcomes Player 1
Defect (5, −2) (0, 0)
are commonly specified through a payoff matrix, such as the one in
Table A.1. Table A.1: Example payoff matrix for
To read the matrix, Player 1’s actions are outlined at the left and a prisoner’s dilemma. This particular
payoff matrix corresponds to a donation
Player 2’s actions at the top. The entry in each box corresponds to a game in which providing the donation
payoff to each player, depending on their respective actions. For in- (of apples or broccoli, say) costs 2 unit
and receiving the donation provides a
stance, the top-right box indicates the payoff when Player 1 cooperates
benefit of 5 units.
and Player 2 defects. Player 1 receives a payoff of −2 and Player 2 re-
ceives a payoff of 5 in that case.
To see why a dilemma arises, consider the possible actions taken
380 PROGRAMMING WELL
# min_int, max_int ;;
- : int * int = (-4611686018427387904, 4611686018427387903)
The int type can then represent integers with up to 18 or so digits, that
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.
The operating mechanism. . . might act upon other things besides num- Figure A.2: A rare daguerrotype of Ada
ber, were objects found whose mutual fundamental relations could be Lovelace (Augusta Ada King, Countess
of Lovelace, 1815–1852) by Antoine
expressed by those of the abstract science of operations, and which
Claudet, taken c. 1843, around the
should be also susceptible of adaptations to the action of the operating
time she was engaged in writing her
notation and mechanism of the engine. Supposing, for instance, that notes on the Babbage analytical engine.
the fundamental relations of pitched sounds in the science of harmony (Menabrea and Lovelace, 1843)
and of musical composition were susceptible of such expression and
adaptations, the engine might compose elaborate and scientific pieces
of music of any degree of complexity or extent. (Menabrea and Lovelace,
1843, page 694)
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
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.
In this problem set you will work with two new ideas: First, we provide
a bit of practice with imperative programming, emphasizing mutable
data structures and the interaction between assignment and lexical
scoping. Since this style of programming is probably most familiar to
you, this portion of the problem set is brief. Second, we introduce 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.
(a) (b)
A.8.1 Background
• 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.
B.1 Functions
fact(0) = 1
fact(n) = n · fact(n − 1) for n > 0
B.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 B.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 B.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 replace x with each
2 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 the function g in Example I assigns the number 6 to both I
But someone did. In fact, it was the 18th century 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 B.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 B.1.4), a logic of functions. In
the lambda calculus, functions and their application are so central (in-
deed, there’s basically nothing else in the logic) that the addition of the Figure B.2: Leonhard Euler (1707–1783)
parentheses in the function application notation is too onerous. In- invented the familiar parenthesized
notation for function application.
stead, Church proposed merely prefixing the function to its argument.
Instead of f (1), Church’s notation would have f 1. Instead of f (g (1)),
f (g 1).
even named f !
0! = 1
n ! = n · (n − 1)! for n > 0
394 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:
p p
λn. n 2 The function from n to n 2 , that is, the absolute Table B.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)
2
λm.(m · n ) 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
define anonymous functions, so central to functional programming
languages, is inherited directly from the lambda notation that gives its
name to Church’s calculus.
As shown in Table B.1, each of the examples above could be
rephrased in OCaml. You may recognize the last of these as an example
of a curried function (Section 6.2).
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.
B.2 Summation
We can think of this sum as adding all the values from 1 to n, or con-
versely, all the numbers from n to 1, that is all the values of (n − i + 1):
n
X
S= (n − i + 1)
i =1
but the two sums can be brought together as a single sum and simpli-
fied:
n
X
2S = (i + (n − i + 1))
i =1
X n
= (n + 1)
i =1
2S = n · (n + 1)
M AT H E M AT I C A L B A C KG R O U N D A N D N OTAT I O N S 397
so that
n · (n + 1)
S=
2 p q p and q p or q not p
For Gauss’s problem, where n is 100, he presumably calculated true true true true false
100·101 true false false true false
2 = 5050. false true false true true
false false false false true
The logic of propositions, boolean logic, underlies the bool type. In-
formally, propositions are conceptual objects that can be either true or
false. Propositions can be combined or transformed with various oper-
ations. The C O N J U N C T I O N of two propositions p and q is true just in
hypotenuse
case both p and q are true, and false otherwise. The D I S J U N C T I O N is B
of the “and” of two boolean values, or their “or”. (See Figure B.5.)
Figure B.6: A right triangle. Angle C is a
There are other operations on boolean values considered in logic – right angle. The opposite side, of length
for instance, the conditional, glossed by “if . . . then . . . ”; or the exclusive c, is the hypotenuse. By Pythagorus’s
theorem, a 2 + b 2 = c 2 .
“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 )
B.4 Geometry
(x2 x1 )2 + (y2 y1 )2
y2 y1
The S L O P E of a line between two points x 1 , y 1 and x 2 , y 2 is the ratio
y 2 −y 1
of their vertical difference and their horizontal difference, x 2 −x 1 . (See (x1 , y1 ) x2 x1
Figure B.7.)
A R I G H T T R I A N G L E is a triangle one of whose edges is a right (90◦ )
Figure B.7: Two points, given by a pair
angle. (See Figure B.7.) The side opposite the right angle is called the of their x (horizontal) and y (vertical)
H Y P OT E N U S E . P Y T H A G O RU S ’ S T H E O R E M holds that the sum of the coordinates. The slope of the line
y −y
between them is x2 −x1 . The distance
squares of the adjacent sides’ lengths is the square of the length of the between them, as
2 1
per the Pythagorean
hypotenuse.
q
theorem, is (x 2 − x 1 )2 + (y 2 − y 1 )2 .
Pythagorus’s theorem can be used to determine the D I S TA N C E
between two points specified with Cartesian (x-y) coordinates. As
depicted in Figure B.7, by Pythagorus’s theorem, we can square the c
differences in each dimension, sum the squares, and take the square r
root. o
A d
The ratio of the circumference of a circle and its diameter is (non-
trivially, and perhaps surprisingly) a constant, conventionally called π
(read, “pi”), and approximately 3.1416. This constant is also the ratio Figure B.8: Geometry of the circle at
of the area of a circle to the area of a square whose side is the circle’s origin o of radius r , diameter d = 2r ,
circumference c, and area A.
398 PROGRAMMING WELL
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;
x[x 7→ P ] = P
y[x 7→ 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.
C.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.
✗ 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.
C.1.7 Indentation
✓
if exp1 then veryshortexp2 else veryshortexp3
When the branches are too long for a single line, move the else onto its
own line.
✓
if exp1 then exp2
else exp3
✓
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.
✓
if exp1 then
longexp2
else shortexp3
✓
if exp1 then
longexp2
else
longexp3
Some use an alternative conditional layout, with the then and else
keywords starting their own lines.
✗
if exp1
then exp2
else exp3
✓ let x = definition in
code_that_uses_x
✗ 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
C.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 407
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 C.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.
408 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 C.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 409
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.
✗ let succ x = x + 1
✗ let rec zip3 (x : 'a list) (y : 'b list) (z : 'c list) : ('a * 'b *
'c) list option =
...
410 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 411
module L = List
type foo = int
exception InternalError
let first list = L.nth list 0
✗ let f arg1 =
let x = arg1.foo in
let y = arg1.bar in
let baz = arg1.baz in
...
✗ match e with
| true -> x
| false -> y
✓ if e then x else y
and
✗ match e with
| c -> x (* c is a constant value *)
| _ -> y
✓ if e = c then x else y
A STYLE GUIDE 413
✓ let x, _ = expr in
...
✗ let v = some_function () in
let x = fst v in
let y = snd v in
x + y
✓ 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.
C.5 Verbosity
✓ if e then x else y
✗ ✓
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
✓ if e then y else x
✓
A STYLE GUIDE 415
You can even do this when the function is an infix binary operator,
though you’ll need to place the operator in parentheses.
✓ 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.
✗ ✓
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
D
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
418 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
420 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
# succ ;;
- : int -> int = <fun>
# string_of_int ;;
- : int -> string = <fun>
# not ;;
- : bool -> bool = <fun>
# 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 15 The most direct approach uses two let bind-
ing for the two sides:
Solution to Exercise 18
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 21
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>
Solution to Exercise 23
Solution to Exercise 25
Solution to Exercise 26
Solution to Exercise 34
Solution to Exercise 35
1. bool * int
2. bool * bool
3. int * int
4. float * int
5. float * int
6. int * int
Solution to Exercise 36
# true, true ;;
- : bool * bool = (true, true)
# true, 42, 3.14 ;;
- : bool * int * float = (true, 42, 3.14)
# (true, 42), 3.14 ;;
426 PROGRAMMING WELL
Solution to Exercise 37
Solution to Exercise 39
Solution to Exercise 40
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:
SOLUTIONS TO SELECTED EXERCISES 427
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 44
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 45
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 46
Solution to Exercise 47
428 PROGRAMMING WELL
Solution to Exercise 48
# let rec square_all lst =
# match lst with
# | [] -> []
# | hd :: tl -> (hd * hd) :: square_all tl ;;
val square_all : int list -> int list = <fun>
Solution to Exercise 49
# let rec append (x : int list) (y : int list)
# : int list =
# match x with
# | [] -> y
# | hd :: tl -> hd :: (append tl y) ;;
val append : int list -> int list -> int list = <fun>
Solution to Exercise 50
# let rec concat (sep : string) (lst : string list)
# : string =
# match lst with
# | [] -> ""
# | [hd] -> hd
# | hd :: tl -> hd ^ sep ^ (concat sep tl) ;;
val concat : string -> string list -> string = <fun>
Solution to Exercise 51
# let tesseract = power 4 ;;
val tesseract : int -> int = <fun>
If your definition was longer, you’ll want to review the partial applica-
tion discussion.
Solution to Exercise 52
# let double_all = map (( * ) 2) ;;
val double_all : int list -> int list = <fun>
Solution to Exercise 53
# let rec fold_left (f : int -> int -> int)
# (init : int)
# (xs : int list)
# : int =
# match xs with
# | [] -> init
# | hd :: tl -> fold_left f (f init hd) tl ;;
val fold_left : (int -> int -> int) -> int -> int list -> int =
<fun>
SOLUTIONS TO SELECTED EXERCISES 429
# 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 left-side argument before its right-side argu-
ment, so that the function ((<) 0) is equivalent to fun x -> 0 < x,
that is, the function that returns true for positive integers. Nonethe-
less, 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]
or even
# ((fun _ -> failwith "") : bool * bool -> bool) ;;
- : bool * bool -> bool = <fun>
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
# let f x g = g x ;;
val f : 'a -> ('a -> 'b) -> 'b = <fun>
# ( |> ) ;;
- : 'a -> ('a -> 'b) -> 'b = <fun>
434 PROGRAMMING WELL
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 f x =
# x +. 42. ;;
val f : float -> float = <fun>
SOLUTIONS TO SELECTED EXERCISES 435
# let f g x =
# g (x + 1) ;;
val f : (int -> 'a) -> int -> 'a = <fun>
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 ;;
436 PROGRAMMING WELL
# let f x y =
# x y y ;;
val f : ('a -> 'a -> 'b) -> 'a -> 'b = <fun>
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 (@+) f g x = f (g x) ;;
val ( @+ ) : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>
Solution to Exercise 69
SOLUTIONS TO SELECTED EXERCISES 439
# let sum =
# List.fold_left (+) 0 ;;
val sum : int list -> int = <fun>
Solution to Exercise 73
# List.hd ;;
- : 'a list -> 'a = <fun>
# List.tl ;;
- : 'a list -> 'a list = <fun>
Alternatively, the check could have been done inside the second match
statement. Why might this be the dispreferred choice?
Solution to Exercise 77
# 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>
# | _, _ -> None
# | xhd :: xtl, yhd :: ytl ->
# match zip_opt' xtl ytl with
# | None -> None
# | Some ztl -> Some ((xhd, yhd) :: ztl) ;;
Line 7, characters 2-24:
7 | | xhd :: xtl, yhd :: ytl ->
^^^^^^^^^^^^^^^^^^^^^^
Warning 11: this match case is unused.
val zip_opt' : 'a list -> 'b list -> ('a * 'b) list option = <fun>
Solution to Exercise 82
Solution to Exercise 84
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>
442 PROGRAMMING WELL
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 85
2. The type of f is bool -> bool * bool. In fact, f always returns the
same value, the pair true, true.
Note that the explicit typing of b is required to force the function type
to be bool -> bool * bool instead of ’a -> bool * bool.
Solution to Exercise 89
# let ( |> ) arg func = func arg ;;
val ( |> ) : 'a -> ('a -> 'b) -> 'b = <fun>
Solution to Exercise 90 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 ;;
SOLUTIONS TO SELECTED EXERCISES 443
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 92.
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.
444 PROGRAMMING WELL
Solution to Exercise 95
# let str_bintree =
# Node ("red",
# Node ("orange",
# Node ("green",
# Node ("blue", Empty, Empty),
# Node ("indigo", Empty, Empty)),
# Empty),
# Node ("yellow",
# Node ("violet", Empty, Empty),
# Empty)) ;;
val str_bintree : string bintree =
Node ("red",
Node ("orange",
Node ("green", Node ("blue", Empty, Empty),
Node ("indigo", Empty, Empty)),
Empty),
Node ("yellow", Node ("violet", Empty, Empty), Empty))
Solution to Exercise 96
Solution to Exercise 97 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
SOLUTIONS TO SELECTED EXERCISES 445
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 104 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 111 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";
448 PROGRAMMING WELL
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 112 Since we only need the float functionality for
weight, a simple definition is best.
# type weight = float ;;
type weight = float
Solution to Exercise 114 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; }
module MobileArg =
struct
type leaft = obj
type nodet = weight
end ;;
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:
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.
Solution to Exercise 123 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.
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) ;;
# | [], _ -> ys
# | _, [] -> xs
# | x :: xst, y :: yst ->
# if lt x y then x :: (merge lt xst ys)
# else y :: (merge lt xs yst)
#
# let rec sort (lt : 'a -> 'a -> bool)
# (xs : 'a list)
# : 'a list =
# match xs with
# | []
# | [_] -> xs
# | _ -> let first, second = split xs in
# merge lt (sort lt first) (sort lt second)
# end ;;
module MergeSort : SORT
- : int = -1
# compare_lengths [1;2;3] [4] ;;
- : int = 1
# compare_lengths [1;2] [3;4] ;;
- : int = 0
Solution to Exercise 154 O(n) – linear. The odds and evens function
are both linear and return a list of length linear in n. The append is
linear in the length of the odds list, so also linear in n. The sum is
linear in the length of its argument, which is identical in length to
(and thus linear in) n. The let body is constant time. Summing these
complexities up, we’re left with linear and constant terms, which is
dominated by the linear term. Hence the function is linear.
Solution to Exercise 155 Let’s start with two mutable values of type
int list ref that are structurally equal but physically distinct:
# lstref1 := [4; 5] ;;
- : unit = ()
# lstref1 = lstref2 ;;
- : bool = false
# lstref1 == lstref2 ;;
- : bool = false
SOLUTIONS TO SELECTED EXERCISES 455
Now for two values that are physically equal (that is, aliases), and
therefore structurally equal as well:
# lstref3 := [4; 5] ;;
- : unit = ()
# 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.)
456 PROGRAMMING WELL
# 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}}
SOLUTIONS TO SELECTED EXERCISES 457
# 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) ;;
458 PROGRAMMING WELL
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
memory. Because s and r are not structurally equivalent, s is still a
reference to p.
Putting it all together, we again evaluate each of the addends in the
expression !p + !(!r) + !(!s); !p and thus !(!r) each evaluate
to 14, while !(!s)) now evaluates to 17. We’re thus left with 14 +
14 + 17, and t = 45.
# let t =
# s := ref 17;
# !p + !(!r) + !(!s) ;;
val t : int = 45
# t ;;
- : int = 45
# let rec mfirst (n: int) (mlst: 'a mlist) : 'a list =
# if n = 0 then []
# else match !mlst with
# | Nil -> []
# | Cons (hd, tl) -> hd :: mfirst (n - 1) tl ;;
val mfirst : 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.
let findloc (dct : dict) (target : key) : int option =
let rec findloc' loc =
if loc >= D.size then None
else
match dct.(loc) with
| Empty -> Some loc
| Element {key; _} ->
(if key = target then Some loc
else findloc' (succ loc)) in
findloc' (D.hash_fn target) ;;
*)
let findloc (dct : dict) (target : key)
(cb_unavailable : unit -> 'a)
(cb_empty : int -> 'a)
(cb_samekey : int -> key -> value -> 'a)
: 'a =
let rec findloc' loc =
if loc >= D.size then cb_unavailable ()
else
match dct.(loc) with
| Empty -> cb_empty loc
| Element {key; value} ->
(if key = target then cb_samekey loc key value
else findloc' (succ loc)) in
findloc' (D.hash_fn target) ;;
#
# let cons hd tl =
# incr constructor_count;
# hd :: tl
#
# let pair first second =
# incr constructor_count;
# first, second
# end ;;
module Metered : METERED
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.
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
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>
# G.set_color this#get_color ;
# G.moveto (this#get_pos.x - w/2)
# (this#get_pos.y - h/2);
# G.draw_string s
# end ;;
class text : point -> string -> display_elt
# 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 182 The solution here makes good use of inheri-
tance rather than reimplementation.
{} ⊢ let x = 3 in let y = 5 in x + y
⇓
¯
¯ {} ⊢ 3 ⇓ 3
¯
¯ {x →
7 3} ⊢ let y = 5 in x + y
¯
¯
¯
¯ ⇓
¯ ¯
¯ ¯ {x 7→ 3} ⊢ 5 ⇓ 5
¯ ¯
¯
¯
¯ ¯ {x →
7 3; y 7→ 5} ⊢ x + y
¯ ¯
¯
¯
¯
¯ ⇓¯
¯ ¯ ¯ {x 7→ 3; y 7→ 5} ⊢ x ⇓ 3
¯ ¯ ¯
¯ ¯ ¯
¯ ¯
¯ ¯ {x 7→ 3; y 7→ 5} ⊢ y ⇓ 5
¯ ¯
⇓ 8
¯ ¯
¯
¯
¯ ⇓8
⇓8
{} ⊢ let x = 3 in let x = 5 in x + y
⇓
¯
¯ {} ⊢ 3 ⇓ 3
¯
¯ {x →
7 3} ⊢ let x = 5 in x + x
¯
¯
¯
¯ ⇓
¯ ¯
¯ ¯ {x 7→ 3} ⊢ 5 ⇓ 5
¯ ¯
¯
¯
¯ ¯ {x →
7 5} ⊢ x + x
¯ ¯
¯
¯
¯
¯ ⇓¯
¯ ¯ ¯ {x 7→ 5} ⊢ x ⇓ 5
¯ ¯ ¯
¯ ¯ ¯
¯ ¯
¯ ¯ {x 7→ 5} ⊢ x ⇓ 5
¯ ¯
⇓ 10
¯ ¯
¯
¯
¯ ⇓ 10
⇓ 10
# let a = 2 in
# let f = (fun b -> a * b) in
470 PROGRAMMING WELL
# let a = 3 in
# f (f a) ;;
- : int = 12
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
⇓ S ′ (l ), S ′
E,S ⊢ P ; Q ⇓
¯
¯ E , S ⊢ P ⇓ (), S ′
(R seq )
¯
¯ E , S ′ ⊢ Q ⇓ vQ , S ′′
¯
⇓ vQ , S ′′
SOLUTIONS TO SELECTED EXERCISES 471
let rec x = D in B
to be equivalent to
E , S ⊢ let x = ref U in (x := D ′ ); B ′
⇓
¯
¯ E , S ⊢ ref U
¯
⇓¯
¯
¯
¯
¯ E,S ⊢U ⇓U,S
¯ ¯
¯
¯
¯
¯ ⇓ l , S{l 7→ U }
¯ E {x 7→ l }, S{l 7→ U } ⊢ (x := D ′ ); B ′
¯
¯
¯
¯ ⇓
¯
¯ E {x →7 l }, S{l 7→ U } ⊢ x := D ′
¯
¯ ¯
¯
⇓
¯ ¯
¯ ¯
¯ ¯
¯
¯ ¯ ¯ E {x 7→ l }, S{l 7→ U } ⊢ x ⇓ l , S{l 7→ U }
¯ ¯ ¯
¯ ¯
¯
¯ ¯ ¯ E {x 7→ l }, S{l 7→ U } ⊢ D’
¯ ¯ ¯
⇓¯
¯ ¯ ¯
¯ ¯ ¯
¯ ¯
¯ ···
¯ ¯ ¯ ¯
¯ ¯ ¯
¯
¯ ¯ ′
⇓ v , S
¯
¯ ¯
¯
¯ D
⇓ (), S ′ {l 7→ v D }
¯ ¯
¯ ¯
¯
¯ E {x 7→ l }, S ′ {l 7→ v D } ⊢ B’
¯ ¯
¯ ¯
¯
⇓¯
¯ ¯
¯ ¯
¯ ¯
¯ ···
¯ ¯ ¯
¯ ¯
′′
¯
⇓ vB , S
¯ ¯
¯
¯
¯ ⇓ v B , S ′′
⇓ v B , S ′′
This schematic derivation is complete, except for the two highlighted
subderivations for D ′ and B ′ respectively. Thus, we can define a se-
mantic rule for the original construct let rec x = D in B (now
with abbreviations expanded) that incorporates these two subderiva-
tions as premises:
E , S ⊢ let rec x = D in B ⇓
¯
¯ E {x 7→ l }, S{l 7→ unassigned} ⊢ D[x 7→ !x] ⇓ v , S ′
¯ D
¯ E {x 7→ l }, S ′ {l 7→ v D } ⊢ B [x 7→ !x] ⇓ v B , S ′′
¯
⇓ v B , S ′′
472 PROGRAMMING WELL
(R letrec )
# sum_bintree int_bintree ;;
- : int = 154
SOLUTIONS TO SELECTED EXERCISES 473
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.