Programming Languages: Build Prove and Compare
Programming Languages: Build Prove and Compare
Computer scientists often need to learn new programming languages quickly. The best way to
prepare is to understand the foundational principles that underlie even the most complicated industrial
languages.
This text for an undergraduate programming-languages course distills great languages and their
design principles down to easy-to-learn “bridge” languages implemented by interpreters whose key
parts are explained in the text. The book goes deep into the roots of both functional and object-oriented
programming, and it shows how types and modules, including generics/polymorphism, contribute to
effective programming.
The book is not just about programming languages; it is also about programming. Through
concepts, examples, and more than 300 practice exercises that exploit the interpreters, students learn
not only what programming-language features are common but also how to do things with them.
Substantial implementation projects include Milner’s type inference, both copying and mark-and-
sweep garbage collection, and arithmetic on arbitrary-precision integers.
Norman Ramsey is Associate Professor of Computer Science at Tufts University. Since earning his
PhD at Princeton, he has worked in industry and has taught programming languages, advanced
functional programming, programming language implementation, and technical writing at Purdue,
the University of Virginia, and Harvard as well as Tufts. He has received Tufts’s Lerman-Neubauer
Prize, awarded annually to one outstanding undergraduate teacher. He has also been a Hertz Fellow
and an Alfred P. Sloan Research Fellow. His implementation credits include a code generator for the
Standard ML of New Jersey compiler and another for the Glasgow Haskell Compiler.
Programming Languages
Build, Prove, and Compare
Norman Ramsey
Tufts University, Massachusetts
University Printing House, Cambridge CB2 8BS, United Kingdom
One Liberty Plaza, 20th Floor, New York, NY 10006, USA
477 Williamstown Road, Port Melbourne, VIC 3207, Australia
314–321, 3rd Floor, Plot 3, Splendor Forum, Jasola District Centre, New Delhi – 110025, India
103 Penang Road, #05–06/07, Visioncrest Commercial, Singapore 238467
www.cambridge.org
Information on this title: www.cambridge.org/highereducation/isbn/9781107180185
DOI: 10.1017/9781316841396
© Norman Ramsey 2023
This publication is in copyright. Subject to statutory exception
and to the provisions of relevant collective licensing agreements,
no reproduction of any part may take place without the written
permission of Cambridge University Press.
First published 2023
A catalogue record for this publication is available from the British Library.
ISBN 978-1-107-18018-5 Hardback
Additional resources for this publication at www.cambridge.org/ramsey
Cambridge University Press has no responsibility for the persistence or accuracy of
URLs for external or third-party internet websites referred to in this publication
and does not guarantee that any content on such websites is, or will remain,
accurate or appropriate.
To Cory, who also knows joy in creation
CONTENTſ
PREFACE ix
ACĸNOWLEDGMENTſ xv
CREDıTſ xvii
TABLEſ OF ȷUDGMENT FORMſ AND ıMPORTANT FUNCTıONſ xviii
SYMBOLſ AND NOTATıON xx
INTRODUCTıON 1
PART I. FOUNDATıONſ
1 AN ıMPERATıVE CORE 11
1.1 Looking at languages 13 1.6 The interpreter 38
1.2 The Impcore language 17 1.7 Operational semantics 55
1.3 Abstract syntax 27 revisited: Proofs
1.4 Environments and the 28 1.8 Extending Impcore 66
meanings of names 1.9 Summary 67
1.5 Operational semantics 29 1.10 Exercises 72
AFTERWORD 727
BıBLıOGRAPHY 733
KEY WORDſ AND PHRAſEſ 745
CONCEPT ıNDEX 749
Preface
The concepts are supported by the bridge languages as shown in the Introduc‐
tion (Table I.2, page 5), which also explains each bridge language in greater detail
(pages 3 to 7).
The book calls for skills in both programming and proof:
ix
To extend and modify the implementations in Chapters 5 to 10, a learner
needs to be able to read and modify Standard ML code; the necessary skills
are developed in Chapters 2, 5, and 8. Standard ML is used because it is a
simple, powerful language that is ideally suited to writing interpreters.
Preface
DEſıGNıNG A COURſE TO UſE THıſ BOOĸ
x
Some books capture a single course, and when using such a book, your only choice
is to start at the beginning and go as far as you can. But in programming languages,
instructors have many good choices, and a book shouldn’t make them all for you.
This book is designed so you can choose what to teach and what to emphasize while
retaining a coherent point of view: how programming languages can be used effec‐
tively in practice. If you’re relatively new to teaching programming languages and
are not sure what to choose, you can’t go wrong with a course on functions, types,
and objects (Chapters 1, 2, 6, 7, and 10). If you have more experience, consider the
ideas below.
Programming Languages: Build, Prove, and Compare gives you interesting, pow‐
erful programming languages that share a common syntax, a common theoretical
framework, and a common implementation framework. These frameworks sup‐
port programming practice in the bridge languages, implementation and extension
of the bridge languages, and formal reasoning about the bridge languages. The de‐
sign of your course will depend on how you wish to balance these elements.
• A second design strategy tilts your class toward programming practice, ei‐
ther de‐emphasizing or eliminating theory. To introduce programming prac‐
tice in diverse languages, Build, Prove, and Compare occupies a sweet spot be‐
tween two extremes. One extreme “covers” N languages in N weeks. This
extreme is great for exposure, but not for depth—when students must work Designing a course
with real implementations of real languages, a week or even two may be to use this book
enough to motivate them, but it’s not enough to build proficiency. xi
The other extreme goes into full languages narrowly but deeply. Students
typically use a couple of popular languages, and overheads are high: each
language has its own implementation conventions, and students must man‐
age the gratuitous details and differences that popular languages make in‐
evitable.
Build, Prove, and Compare offers both breadth and depth, without the over‐
head. If you want to focus on programming practice, you can aim for “four
languages in ten weeks”: µScheme, µML, Molecule, and µSmalltalk. You can
bring your students up to speed on the common syntactic, semantic, and im‐
plementation frameworks using Impcore, and that knowledge will support
them through to the next four languages. If you have a couple of extra weeks,
you can deepen your students’ experience by having them work with the in‐
terpreters.
• A third design strategy tilts your class toward applied theory. Build, Prove,
and Compare is not suitable for a class in pure theory—the bridge languages
are too big, the reasoning is informal, and the classic results are miss‐
ing. But it is suitable for a course that is primarily about using formal
notation to explain precisely what is going on in whole programming lan‐
guages, reinforced by experience implementing that notation. Your stu‐
dents can do metatheory with Impcore, Typed Impcore, Typed µScheme,
and nano‐ML; equational reasoning with µScheme; and type systems with
Typed Impcore, Typed µScheme, nano‐ML, µML, and Molecule. They can
compare how universally quantified types are used in three different designs
(Typed µScheme, nano‐ML/µML, and Molecule).
All of these potential designs are well supported by the exercises (345 in total),
which fall into three big categories. For insight into how to use programming lan‐
guages effectively, there are programming exercises that use the bridge languages.
For insight into the workings of the languages themselves, as well as the formalism
that describes them, there are programming exercises that extend or modify the
interpreters. And for insight into formal description and proof, there are theory ex‐
ercises. Model solutions for some of the more challenging exercises are available
to instructors.
A few exercises are simple enough and easy enough that your students can work
on them for 10 to 20 minutes in class. But most are intended as homework.
Because this book is organized by language, its scope is partly determined by what
the bridge languages do and do not offer relative to the originals on which they are
based.
• µScheme offers define, a lambda, and three “let” forms. Values include sym‐
bols, machine integers, Booleans, cons cells, and functions. There’s no nu‐
meric tower and there are no macros.
• µML offers type inference, algebraic data types, and pattern matching.
There are no modules, no exceptions, no mutable reference cells, and no
value restriction.
Each of the languages supports multiple topical themes; the major themes are
programming, semantics, and types.
• Light metatheory for both operational semantics and type systems (Chap‐
ters 1, 5, and 6)
A book is characterized not only by what it includes but also by what it omits.
To start, this book omits the classic theory results such as type soundness and
strong normalization; although learners can prove some simple theorems and look
for interesting counterexamples, theory is used primarily to express and commu‐
nicate ideas, not to establish facts. The book also omits lambda calculus, because
lambda calculus is not suitable for programming.
The book omits concurrency and parallelism. These subjects are too difficult
and too ramified to be handled well in a broad introductory book.
And for reasons of space and time, the book omits three engaging program‐
ming models. One is the pure, lazy language, as exemplified by Haskell. An‐
other is the prototype‐based object‐oriented language, made popular by JavaScript,
but brilliantly illustrated by Self. The third is logic programming, as exemplified
by Prolog—although Prolog is explored at length in the Supplement (Appendix D).
If you are interested in µHaskell, µSelf, or µProlog, please write to me.
The software described in the book is available from the book’s web site, which is
build‑prove‑compare.net. The web site also provides a “playground” that allows
you to experiment with the interpreters directly in your browser, without having to
download anything. And it holds the book’s PDF Supplement, which includes ad‐
ditional material on multiprecision arithmetic, extensions to algebraic data types,
logic programming, and longer programming examples. The Supplement also de‐
scribes all the code: both the reusable modules and the interpreter‐specific mod‐
ules.
Acknowledgments
xv
Cyrus Cousins found a subtle bug in µScheme+.
Mike Hamburg and Inna Zakharevich spurred me to improve the concrete syn‐
tax of µSmalltalk and to provide better error messages.
Andrew Black examined an earlier design of µSmalltalk and found it want‐
ing. His insistence on good design and clear presentation spurred innumerable
improvements to Chapter 10.
Pharo By Example (Black et al. 2010) explained Smalltalk metaclasses in a way
I could understand.
Dan Grossman read an early version of the manuscript, and he not only com‐
Acknowledgments
mented on every detail but also made me think hard about what I was doing. Kath‐
xvi leen Fisher’s careful reading spurred me to make many improvements through‐
out Chapters 1 and 2. Jeremy Condit, Ralph Corderoy, Allyn Dimock, Lee Feigen‐
baum, Luiz de Figueiredo, Andrew Gallant, Tony Hosking, Scott Johnson, Juergen
Kahrs, and Kell Pogue also reviewed parts of the manuscript. Gregory Price sug‐
gested ways to improve the wording of several problems. Penny Anderson, Jon
Berry, Richard Borie, Allyn Dimock, Sam Guyer, Kathleen Fisher, Matthew Fluet,
William Harrison, David Hemmendinger, Tony Hosking, Joel Jones, Giampiero
Pecelli, Jan Vitek, and Michelle Strout bravely used preliminary versions in their
classes. Penny found far more errors and suggested many more improvements
than anyone else; she has my profound thanks.
My students, who are too numerous to mention by name, found many errors in
earlier drafts. Students in early classes were paid one dollar per error, from which
an elite minority earned enough to recover the cost of their books.
Individual chapters were reviewed by Richard Eisenberg, Mike Sperber, Robby
Findler, Ron Garcia, Jan Midtgaard, Richard Jones, Suresh Jagannathan, John
Reppy, Dimitrios Vytiniotis, François Pottier, Chris Okasaki, Stephanie Weirich,
Roberto Ierusalimschy, Matthew Fluet, Andreas Rossberg, Stephen Chang, Andrew
Black, Will Cook, and Markus Triska.
Larry Bacow inspired me to do the right thing and live with the consequences.
Throughout the many years I have worked on this book, Cory Kerens has loved
and supported me. And during the final push, she has been the perfect companion.
She, too, knows what it is to be obsessed with a creative work—and that shipping is
also a feature. Cory, it’s time to go adventuring!
Credits
xvii
Judgment forms, important functions, & concrete syntax
Evaluation judgments
Typing judgments
Wellformedness judgments
Other judgments
Concrete syntax
Impcore
::= defines a syntactic category in a grammar, page 17
Notation separates alternatives in a grammar, page 17
··· repeatable syntax in a grammar, page 17
xx
ξ global‐variable environment (“ksee”), page 28
ϕ function environment (“fee”), page 29
ρ value environment (“roe”), page 29
x object‐language variable, page 29
v value, page 29
7 → shows binding in function or environment, page 29
y object‐language variable, page 29
{} empty environment, page 29
d definition, page 30
e expression, page 30
h· · · i brackets wrapping abstract‐machine state, page 30
⊕ object‐language operator, page 30
⇓ relates initial and final states of big‐step evaluation (“yields”), page 30
dom domain of an environment or function, page 32
∈ membership in a set, page 32
f name of object‐language function, page 36
→ relates initial and final states in evalution of definitions, page 37
△
= defines syntactic sugar, page 66
· ·]]
[[· brackets used to wrap syntax (“Oxford brackets”), page 81
··· optional syntax in a grammar, page 86
µScheme
P in a mini‐index, marks a primitive function (“primitive”), page 95
O(· · · ) asymptotic complexity, page 100
k a key in an association list, page 105
a an attribute in an association list, page 105
{· · · } justification of a step in an equational proof, page 114
(|· · · |) a closure, page 122
◦ function composition (“composed with”), page 125
:: infix notation for cons (“cons”), page 128
∨ disjunction (“or”), page 139
¬ Boolean complement (“not”), page 139
σ the store: a mapping of locations to values (“sigma”), page 144
⊆ the subset relation, reflexively closed (“subset”), page 181
µScheme+
[] an empty stack (“empty”), page 210
F frame on an evaluation stack (“frame”), page 210
S evaluation stack, page 210
• a hole in an evaluation context (“hole”), page 211
e ⇝ e′ lowering transformation (“lowerexp”), page 214
→ the reduction relation in a small‐step semantics (“steps to”), page 215
e/v abstract‐machine component: either an expression or a value, page 215
→∗ the reflexive, transitive closure of the reduction relation
(“normalizes to”), page 215
C an evaluation context in a traditional semantics, page 241
λ the Greek way of writing lambda, page 242
Garbage collection
H the size of the heap, page 260 Notation
L the amount of live data, page 261
γ the ratio of heap size to live data (“gamma”), page 262 xxi
Type systems
τ a type (“tau”), page 333
Γ type environment; maps term variable to its type (“gamma”), page 333
→ in a function type, separates the argument types from the result type
(“arrow”), page 334
× in a function type, separates the types of the arguments (“cross”), page 334
` in a judgment, separates context from conclusion (“turnstile”), page 335
e:τ ascribes type τ to term e (“e has type τ ”), page 335
→ relates type environments before and after typing of definition, page 336
µ a type constructor (“mew”), page 347
× forms pair types or product types (multiplication is · on page S15)
(“cross”), page 348
+ used to form sum types, page 349
[[τ ]] the set of values associated with type τ , page 350
κ a kind, which classifies types (“kappa”), page 354
∗ the kind ascribed to types that classify terms (“type”), page 354
⇒ used to form kinds of type constructors (“arrow”), page 354
τ :: κ ascribes kind κ to type τ (“τ has kind κ”), page 354
∆ a kind environment (“delta”), page 354
α, β, γ type variables (“alpha, beta, gamma”), page 356
∀ used to write quantified, polymorphic types (“for all”), page 356
(τ1 , . . . , τn ) τ τ applied to type parameters τ1 , . . . , τn , page 357
≡ type equivalence, page 369
∩ set intersection, page 374
∅ the empty set (“empty”), page 374
Type inference
σ a type scheme (“sigma”), page 408
θ a substitution (“THAYT‐uh”), page 409
⩽ the instance relation (“instance of”), page 410
θI the identity substitution, page 411
τ ∼ τ′ simple type‐equality constraint (“τ must equal τ ′ ”), page 418
C type‐equality constraint, page 418
T the trivial type‐equality constraint, page 420
≡ equivalence of constraints, page 432
Programming Languages: Build, Prove, and Compare helps you use programming lan‐
guages effectively, describe programming languages precisely, and understand and
enjoy the diversity of programming languages. You will learn by experimenting
with and comparing code written in different languages. You will use important
programming‐language features to write interesting code, understand how each
feature is implemented, and see how different languages are similar and how each
one is distinctive.
You will code in and experiment with small bridge languages, which illuminate
essential features that you will see repeatedly throughout your career. Each bridge
language is small enough to learn, but big enough to act as a bridge to the real thing.
The main bridge languages—µScheme, µML, Molecule, and µSmalltalk—are rich
enough to write programs that are interesting, and they are distilled from languages
1
whose greatness is widely acknowledged: designers behind Scheme, ML, CLU, and
Smalltalk have all won ACM Turing Awards, which is the highest professional honor
a computer scientist can receive. Their designs have influenced many languages
that are fashionable today, including Racket, Clojure, Rust, Haskell, Python, Java,
JavaScript, Objective C, Ruby, Swift, and Erlang.
Four other bridge languages are suitable for writing toy programs only: Imp‐
core, Typed Impcore, Typed µScheme, and nano‐ML are intended for conveying
ideas, not for programming.
All the bridge languages share the same, simple concrete syntax, in which every
Introduction
expression is wrapped in parentheses. Uniform syntax helps you ignore superficial
2 differences and focus on essentials. Each bridge language is also implemented by
an interpreter, which runs the code you write. The interpreter helps you master
the abstract world of formalism—in each chapter, you can compare mathematical
descriptions of language ideas with the code that implements those ideas. And you
can use the interpreters to create your own language designs. Whether your own
design explores a variation on one of mine or goes in a completely different di‐
rection, trying new design ideas for yourself—and programming with the results—
will give you a feel for the problems of language design, which you can’t get just by
studying existing languages. Don’t let other people have all the fun!
The book helps you learn in three ways:
• Build, and learn by doing. You will learn by building and modifying programs.
You will write code in the bridge languages, and you will modify the inter‐
preters.
Once you build things, you may want to share them with others, such as po‐
tential employers. My own students’ work is more than worthy of a profes‐
sional portfolio. But if you share your work using a public site like Github or
Bitbucket, please share this part of your portfolio only with individuals that
you name—please don’t put your work in a public repository.
• Prove, to keep things simple and precise. A proof enables an expert to know that
an optimized program behaves the same as the original, or that no program
in a safe language can ever result in an unexplained core dump. Practice with
proof will help you understand what you can and can’t count on from a lan‐
guage and its implementation. Try some exercises in language metatheory
(Chapter 1), equational reasoning (Chapters 2 and 8), and type‐system meta‐
theory (Chapters 6 and 7).
• Compare, and find several ways to understand. We all learn more easily when
we compare new ideas with what we already know—and with each other. You
will learn syntax, for example, by seeing it in two forms: concrete and ab‐
stract. (Concrete syntax says how a language is written; it’s something every
programmer learns. Abstract syntax, which may be new to you, says what the
underlying structure of a language is; it’s the best way to think about what a
language can say.) You can compare these two ways of writing one syntax,
and you can also compare the syntaxes of different languages. The syntax of
each new bridge language uses a new form only when it is needed to express
a new feature; if a feature is found in multiple bridge languages, it is written
using the same syntax each time. Things look different only when they are
different.
To learn about the meanings of language constructs, you can compare an in‐
terpreter with an operational semantics. Learning about interpretation and
operational semantics together is easier than learning about each separately.
To learn powerful programming techniques, including recursion, higher‐
order functions, and polymorphism, you can compare example programs
both large and small. At first you’ll compare example programs written in
a single language, but eventually you will also compare examples written in
different languages.
You’ll accomplish all this by doing exercises. In each chapter, exercises are orga‐
nized by the skills they require or develop, with cross‐reference to the most relevant
sections. And the exercises are preceded by short questions intended for “retrieval
practice,” which helps bring knowledge to the front of your mind. Doing exercises The book in detail
will help you learn how a semantics is constructed, how an interpreter works, and 3
most important, how to write great code. Each of these avenues to learning rein‐
forces the others, and you can emphasize what suits you best.
Don’t try to read this book cover to cover. Instead, choose languages and chap‐
ters that work for you. To help you choose, I introduce the languages and chap‐
ters here. The introductions sometimes use jargon like “operational semantics,”
“polymorphism,” or “garbage collection,” because such jargon tells an expert ex‐
actly what’s here. If you’re not expert yet, don’t worry—there are also some longer
explanations. And use the figures! Figure I.1 (on the next page) shows how the
later chapters depend on earlier ones, and Tables I.2 and I.3 summarize, respec‐
tively, the main theory concepts and programming techniques for each language.
All three, like the book, are divided into two parts: foundational features and fea‐
tures for programming at scale.
Foundations
Technical study starts with abstract syntax and operational semantics, which specify
what a language is and what it does. These specifications are implemented by def
initional interpreters. The first specifications and implementation are presented in
the context of a tiny procedural language, Impcore, which is the subject of Chapter 1.
Impcore includes the familiar imperative constructs that are found at the core of
mainstream programming languages: loops, conditionals, procedures, and muta‐
ble variables. Impcore doesn’t introduce any new or unusual language features;
instead it introduces the professional way of thinking about familiar language fea‐
tures in terms of abstract syntax and operational semantics. Impcore also intro‐
duces the interpreters.
Using abstract syntax, operational semantics, and a definitional interpreter,
µScheme (Chapter 2) introduces two new language features. First, it introduces
Sexpressions, a recursive datatype. S‐expressions are most naturally processed
using recursion, not iteration; this change has far‐reaching effects on program‐
ming style. µScheme also introduces firstclass, nested functions, which are treated
as values, can be stored in data structures, can be passed to functions, and can
be returned from functions. Functions that accept or return functions are called
higherorder functions, and their use leads to a concise, powerful, and distinctive
programming style: functional programming. µScheme is used to explore simple
recursive functions, higher‐order functions, standard higher‐order functions on
lists, continuation‐passing style, and equational reasoning. These new ideas re‐
quire only a handful of new language features and primitive functions: µScheme
extends Impcore by adding let, lambda, cons, car, cdr, and null?.
Ch. 1
Impcore
ABſTRACT ſYNTAX
BıG‐ſTEP, NATURAL‐DEDUCTıON ſEMANTıCſ
DEFıNıTıONAL ıNTERPRETERſ
FOUNDATIONS
′
he, ρ, σi ⇓ hv, σ i
FAMıLıAR PROCEDURAL PROGRAMMıNG
Ch. 4
TYPE ſYſTEMſ Γ`e:τ
GARBAGE COLLECTıON
PROGRAMMING AT SCALE
Languages shown in heavy boxes are suitable for coding,
and the thick, gray arrows show which chapters are pre‐
requisite for which others.
Garbage collection enables programs written in Scheme and other safe lan‐
guages to allocate new memory as needed, without worrying about where memory
comes from or where it goes. Garbage collection simplifies both programming and
interface design, and it is a hallmark of civilized programming. It supports all the
other languages in the book. In Chapter 4, you can learn about garbage collection
by building both mark‐and‐sweep and copying garbage collectors for µScheme+.
You can even build a simple generational collector. If you master Chapters 1 to 4,
you will have substantial experience connecting programming‐language ideas to
interpreters.
Independent of µScheme+ and garbage collection, you can proceed directly
from µScheme to type systems. Type systems demand a change in the implementa‐
tion language: while C is a fine language for writing garbage collectors, it is not so
good for writing type checkers or sophisticated interpreters. Such tools are more
easily implemented in a language that provides algebraic data types and pattern
matching. The simplest, most stable, and most readily available such language
is Standard ML, which is used from Chapter 5 onward. To acclimate you to Stan‐
dard ML, Chapter 5 reimplements µScheme using Standard ML. That reimplemen‐
tation provides infrastructure used in subsequent chapters, including chapters on
type systems.
In Chapter 6, type systems are presented for two languages: Typed Impcore, a
monomorphic, statically typed dialect of Impcore, and Typed µScheme, a polymor‐
phic, statically typed dialect of µScheme. Both type systems illustrate formation
rules, introduction rules, and elimination rules, with connections to logic. And in
the exercises, both systems can be implemented by type checkers. A type checker
embodies the rules by using type annotations, on formal parameters and else‐
where, to determine the type of every expression in a program.
Typed µScheme is super expressive, and its type system, when suitably spe‐
cialized or extended, can describe many real languages, from the simple Hindley‐
Milner types of Standard ML to complex features like Haskell type classes or Java
generics. But considered as a programming language, Typed µScheme is most un‐
pleasant: it requires a type annotation not just on every function definition, but on
every use of a polymorphic function. A better approach is to add type annotations
automatically, using type inference.
Table I.3: Programming technique in each bridge language
Programming at scale
Operational semantics, functions, and types are everywhere. These concepts pro‐
vide a foundation for the second part of the book, which presents mechanisms that
programmers rely on when working at scale. These mechanisms revolve around
data abstraction, which hides representations that are likely to change. Data ab‐
straction enables components of large systems to be built independently and to
evolve independently.
Representations worth hiding need more ways of structuring data than just
the arrays, lists, and atomic types found in Typed Impcore, Typed µScheme, and
nano‐ML. To structure arbitrarily sophisticated representations, a language needs
grouping (like struct), choice (like union), and recursion. All three capabilities
are combined in inductively defined algebraic data types. In Chapter 8, algebraic
data types are demonstrated using µML. µML can define algebraic data types and
can inspect their values using case expressions and pattern matching. These ideas
are central to languages like Haskell, Standard ML, OCaml, Scala, Agda, Idris, and
Coq/Gallina.
Once defined, representations can usefully be hidden using abstract data types,
objects, or both. In Chapter 9, abstract data types are demonstrated using Molecule,
which builds on µML’s algebraic data types. Abstract types are defined inside mod
ules, and they hide information using types: a representation of abstract type can
be accessed only by code that is in the same module as the type’s definition. Access
is controlled by a polymorphic type checker like the one in Typed µScheme. Ab‐
stract types and modules are found in languages as diverse as CLU, Modula–2, Ada,
Oberon, Standard ML, OCaml, and Haskell. (Molecule is a new design inspired by
Modula–3, OCaml, and CLU.)
In Chapter 10, objects are demonstrated using µSmalltalk. Objects hide infor‐
mation using names: the parts of an object’s representation can be named only by
code that is associated with that object, not by code that is associated with other
objects. Unlike such hybrid languages as Ada 95, Java, C#, C++, Modula‐3, Objec‐
tive C, and Swift, µSmalltalk is purely object‐oriented: every value is an object, and
the basic unit of control flow is message passing. Any message can be sent to any
Parting advice
object. Objects are created by sending messages to classes, which are also objects,
and classes inherit state and implementation from parent classes, which enables 7
new forms of code reuse. The mechanisms are simple, but remarkably expressive.
PARTıNG ADVıCE
This book is not a meal; it’s a buffet. Don’t try to eat the whole thing. Pick out a
few tidbits that look appetizing, taste them, and do a few exercises. Digest what
you’ve learned, rest, and repeat. If you work hard, then you, like my students, will
be impressed at how much skill and knowledge you develop, and how you’ll be able
to apply it even to languages you will have never seen before.
PART I. FOUNDATıONſ
CHAPTER 1 CONTENTſ
1.1 LOOĸıNG AT LANGUAGEſ 13 1.7.2 Proofs about derivations:
Metatheory 59
1.2 THE IMPCORE LANGUAGE 17
1.7.3 How to attempt a
1.2.1 Lexical structure and metatheoretic proof 61
concrete syntax 17 1.7.4 Why bother with
1.2.2 Talking about syntax: semantics, proofs, theory,
Metavariables 19 and metatheory? 65
1.2.3 What the syntactic forms do 20
1.2.4 What the primitive
1.8 EXTENDıNG IMPCORE 66
functions do 23 1.9 SUMMARY 67
1.2.5 Extended definitions: 1.9.1 Key words and phrases 67
Beyond interactive 1.9.2 Further reading 71
computation 24
1.10 EXERCıſEſ 72
1.2.6 Primitive, predefined, and
1.10.1 Retrieval practice and
basis; the initial basis 26
other short questions 73
1.3 ABſTRACT ſYNTAX 27 1.10.2 Simple functions using
loops or recursion 73
1.4 ENVıRONMENTſ AND THE
1.10.3 A simple recursive
MEANıNGſ OF NAMEſ 28
function 75
1.5 OPERATıONAL ſEMANTıCſ 29 1.10.4 Working with decimal and
1.5.1 Judgments and rules of binary representations 75
inference 30 1.10.5 Understanding syntactic
1.5.2 Literal values 32 structure 76
1.5.3 Variables 32 1.10.6 The language of
1.5.4 Assignment 34 operational semantics 77
1.10.7 Operational semantics:
1.5.5 Control flow 34
Facts about particular
1.5.6 Function application 36
expressions 77
1.5.7 Rules for evaluating
1.10.8 Operational semantics:
definitions 37
Writing new rules 80
1.6 THE ıNTERPRETER 38 1.10.9 Operational semantics:
1.6.1 Interfaces 41 New proof systems 81
1.6.2 Implementation of 1.10.10 Metatheory: Facts about
the evaluator 48 derivations 83
1.6.3 Implementation of 1.10.11 Advanced metatheory:
environments 54 Facts about
implementation 84
1.7 OPERATıONAL ſEMANTıCſ 1.10.12 Implementation: New
REVıſıTED: PROOFſ 55 semantics for variables 86
1.7.1 Proofs about evaluation: 1.10.13 Extending the interpreter 87
Theory 56 1.10.14 Interpreter performance 87
An imperative core 1
Von Neumann programming languages use variables to
imitate the computer’s storage cells; control statements
elaborate its jump and test instructions; and assignment
statements imitate its fetching, storing, and arithmetic. . .
Each assignment statement produces a oneword result.
The program must cause these statements to be executed
many times in order to make the desired overall change in
the store, since it must be done one word at a time.
John Backus, Can Programming Be Liberated from the
von Neumann Style?
In your prior programming experience, you may have used a procedural language
such as Ada 83, Algol 60, C, Cobol, Fortran, Modula‐2, or Pascal. Or you may have
used a procedural language extended with object‐oriented features, such as Ada 95,
C#, C++, Eiffel, Java, Modula‐3, Objective C, or Python—although these hybrid lan‐
guages support an object‐oriented style, they are often used procedurally. Proce‐
dural programming is a well‐developed style with identifiable characteristics:
• Data is processed one word at a time; words are commonly organized into
arrays or records. An array is typically processed by a loop, and a record
is typically processed by a sequence of commands; these control constructs
reflect an element‐by‐element approach to data processing. Loops are typ‐
ically written using “structured” looping constructs such as for and while;
recursive procedures are not commonly used.
• Both control and data mimic machine architecture. The control constructs
if and while combine conditional and unconditional jump instructions
in simple ways; goto, when present, exposes the machine’s unconditional
jump. Arrays and records are implemented by contiguous blocks of mem‐
ory. Pointers are addresses; assignment is often limited to what can be ac‐
complished by a single load or store instruction.
11
Mimicking a machine has its advantages: costs can be easy to predict, and a
debugger can be built by adapting a machine‐level debugger.
The procedural style can be embodied in a language. In this chapter, that language
The arrow “‑> ” is the interpreter’s prompt; text following a prompt is my input;
and text on the next line is the interpreter’s response:1
To continue, let’s define a function, which I’ll name x‑3‑plus‑1. It multiplies a
number k times 3, then adds 1 to the product:
12b. htranscript 12ai+≡ ◁ 12a 12c ▷
‑> (define x‑3‑plus‑1 (k)
(+ (* 3 k) 1)) ;; returns 3 * k + 1
The syntax may look different from what you are used to, including the dashes
in the function’s name, but before we dive into the differences, let’s compute with
the variable n and function x‑3‑plus‑1 that I just defined. I use them in a loop
that tries to reduce n to 1 by halving n when it is even and replacing it with 3n + 1
when it is odd. (This loop is believed to terminate for any positive n, but at press
time, no proof is known.) In the loop, each line of Impcore code is commented with
analogous C code on the right; if you know C, C++, Java, JavaScript, or something
similar, the comments should help you interpret Impcore’s syntax.
12c. htranscript 12ai+≡ ◁ 12b 21a ▷
‑> (begin ;; {
(while (> n 1) ;; while (n > 1)
(begin ;; {
(println n) ;; printf("%d\n", n);
(if (= (mod n 2) 0) ;; if (n % 2 == 0)
(set n (/ n 2)) ;; n = n / 2;
(set n (x‑3‑plus‑1 n))))) ;; else n = x_3_plus_1(n); }
n) ;; return n; }
3
10
5
16
8
4
2
1
A program is ultimately formed from the individual characters of its code. And
before code runs, it passes through several different phases, each of which is gov‐
erned by its own rules. Phases and their rules can be hard to identify, but for any
programming language, three sets of rules are essential. They tell us
To highlight these rules, let’s look at some code that breaks them. Because some of
the rules involve type checking, we’ll look at some C code. (If C is not so familiar,
try thinking about Java, which has the same kinds of rules.)
Imagine that I have a two‐dimensional point on the plane, and that I want to
add 3 to its x coordinate. In C, a point p can be defined like this:
p. = p.x + 3;
This code is rejected by the C compiler, which reports a syntax error. The compiler
might flag the = sign after the dot, where it would prefer a name. The code is ill
formed.
I know I want the = sign, and if I’m programming after midnight, I might just
remove the dot: mod 27c
p = p.x + 3;
This code is well formed, but a C compiler will report a type error: p is not the type
of thing you can assign a number to. The code is ill typed.
The code I meant to write is
p.x = p.x + 3;
This code is well formed and well typed, and the compiler is happy. But a happy
compiler doesn’t guarantee a happy program. Suppose I write
int n = 0;
p.x = p.x / n;
1 This code is also well formed and well typed, so it makes the compiler happy, but
when it runs, nothing good will happen. The best I can hope for is that my operating
system will report a runtime error, maybe a “floating‐point exception.” (The exam‐
ple exhibits what the C standard calls “undefined behavior,” which the system is
An imperative core not obligated to report.) The code is ill behaved.
Useful code is well formed, well typed, and well behaved. Learning precisely
14
what that means, for several different languages, is half of this book. (The other half
is learning how to use the languages effectively.) The key concepts are as follows:
• Code is formed according to two sets of rules: Characters are clumped into
groups called tokens according to lexical rules, and tokens are grouped into
definitions, statements, and so on according to syntactic rules. The syntactic
rules are the important ones. Syntax is so important that we talk about two
varieties: concrete syntax, which is how we write the code, and abstract syntax,
which is how we think about the code’s structure. Impcore’s concrete and
abstract syntax are presented in Sections 1.2 and 1.3, respectively.
• Although there are myriad ways that code can be checked before it is deemed
OK to run, the most practical method is type checking, which follows the rules
of a static type system. Impcore does not have a static type system; type check‐
ing and its companion, type inference, are not explored in depth until Chap‐
ters 6 and 7.
2. Look for familiar syntactic categories (page 16), like definitions, declarations,
statements, expressions, and types.
3. In each category, look for familiar forms: loops, conditionals, function appli‐
cations, and so on. To identify what’s familiar, mentally translate concrete
syntax into abstract syntax.
This approach calls for an understanding of lexical structure, grammars, and the
two varieties of syntax.
Lexical structure
Lexical structure rarely requires much thought. If you can spot comments, string
literals, and token boundaries, you’re good to go. For example, in C, you need to
see 3*n+1 as 5 tokens, "3*n+1" as a single token (a string literal), and /*3*n+1*/ as
no tokens at all ( just a comment). In this book’s bridge languages, a comment be‐
gins with a semicolon, there are no string literals, and token boundaries are found
§1.1
only at brackets or whitespace. For example, in Impcore, 3*n+1 is a single token
Looking at
(a name).
languages
Grammars 15
Concrete syntax is specified using a grammar, which tells us what sequences of to‐
kens are well formed. For example, the following toy grammar shows four ways to
form an expression exp:
The grammar says that an expression may be a variable, a numeral, the sum of two
expressions, or the product of two expressions. Each alternative is a syntactic form.
This grammar, like any grammar, can be used to produce an expression by re‐
placing exp with any of the four alternatives on the right‐hand side, and continuing
recursively until every exp has been replaced with a variable or a numeral. For ex‐
ample, the C expression 3 * n + 1 can be produced in this way.
The toy grammar is compositional: big expressions are made by composing
smaller ones. Every interesting programming‐language grammar supports some
kind of composition; even an assembly language allows you to compose long se‐
quences of instructions by concatenating shorter sequences. Compositional syn‐
tactic structure is part of what makes something a programming language. More
compositional structure can be found in the grammar for Impcore (page 18).
When we write concrete syntax, we should be thinking about abstract syntax. For ex‐
ample, concrete syntax tells us that the “3n + 1” loop is written using a while key‐
word, and that in C the loop condition goes between round brackets (parentheses).
But we should be thinking “while loop with a condition and a body,” which is what
abstract syntax tells us: it names the form (WHıLE) and says that a WHıLE loop is
formed from an expression (the condition) and a statement (the body). Abstract
syntax ignores syntactic markers like keywords and brackets.
Abstract syntax helps us recognize familiar forms even when they are clothed in
unfamiliar concrete syntax. For example, you can probably recognize loops written
in C, Icon, Impcore, Python, Modula‐3, Scala, and Standard ML:
while (n > 1) n = n / 2;
while n > 1 do n := n / 2
(while (> n 1) (set n (/ n 2))
while n > 1: n = n / 2
WHILE n > 1 DO n := n / 2 END
while (n > 1) { n = n / 2; }
while !n > 1 do n := !n div 2
Each language uses a different concrete syntax, but abstract syntax provides a kind
of X‐ray vision: under their clothes, all these loops are the same.
Abstract syntax is not just a tool for thought. It also gives us abstractsyntax
trees, the data structure used to represent code in most compilers and interpreters.
In this book, abstract‐syntax trees appear in C code (Chapters 1 to 3) and in Stan‐
dard ML code (Chapters 5 to 10). Abstract syntax also provides a compact notation
for operational semantics and type systems. Using such a notation, all the while
loops above would be written the same way: WHıLE(e, s), where e stands for the
1 condition and s stands for the body.
Abstract syntax is rarely specified explicitly; you’ll almost always infer it by read‐
An imperative core
ing a grammar that describes concrete syntax. A real grammar will have many
16 left‐hand sides like exp; these symbols are called nonterminal symbols, or just non
terminals. Sometimes the nonterminals tell you exactly what the important phrases
in a language are, but if the grammar has been engineered primarily to help a com‐
piler to convert its input into an abstract‐syntax tree, many nonterminals will be
annoying or distracting. For example, a nonterminal like explist1 (a list of one or
more expressions separated by commas) adds nothing to our understanding.
To understand the structure of a grammar, search the nonterminals for syntactic
categories. A syntactic category is a group of syntactic forms that share an important
role; for example, the role of an expression is to be evaluated to produce a value
(and possibly also have a side effect). Typically, two phrases in the same syntactic
category can be interchanged without affecting the well‐formedness of a program.
For example, any of these expressions could be used on the right‐hand side of the
assignment to p.x:
Assigning any of these expressions to p.x would be well formed, but as shown
above, the division p.x / n might not be well behaved, and the assignments of
p‑>next and 2 * p aren’t well typed.
Syntactic categories aren’t arbitrary, and in any programming language, there
are at least four common categories worth looking for:
• A definition introduces a new thing and gives it a name. Forms to look for
include forms that define functions, variables, and maybe types; Impcore
marks its function‐definition and variable‐definition forms with keywords
define and val.
• A declaration introduces a new thing, with a name, but doesn’t yet define
the thing—instead, a declaration promises that the thing is defined else‐
where. Because anything that can be declared eventually has to be defined,
the forms to look for are forms for declaring things that can be defined.
In C and C++, declaration forms are mostly found in .h files. Impcore has
no declaration forms; in this book, declaration forms aren’t used until Chap‐
ter 9.
• An expression is evaluated to produce a value, and possibly also have a side ef‐
fect. Forms to look for include variables, literal values, function applications,
maybe infix operators, and hopefully a conditional form (like C’s ternary ex‐
pression e1 ? e2 : e3 ). Impcore has all these forms except infix operators.
• A statement is executed for side effect; it doesn’t produce a value. Side effects
might include printing, changing the value of a variable, or changing some
value in memory, among others. Forms to look for include loops (while, for),
conditionals (if), sequencing (begin), and if you’re lucky, a case or switch
statement. Impcore doesn’t actually have statements; its while and if forms
are expressions.
Impcore’s lack of statements might surprise you, but it’s a well‐known design
choice. Providing loops and conditionals as expressions, not statements, makes
a language expressionoriented. All functional languages are expression‐oriented;
among procedural languages, Icon is expression‐oriented (Griswold and Griswold
1996); and among languages with object‐oriented features, Scala is expression‐
oriented (Odersky, Spoon, and Venners 2019). Making a language expression‐
oriented simplifies the syntax a bit; for example, an expression‐oriented language
§1.2
needs only one conditional form, whereas a language like C has both a conditional
The Impcore
statement and a conditional expression.
language
With your eye out for familiar definition and expression forms, you’re ready to
be fully introduced to Impcore. 17
Impcore fits the model described on page 14: the rules for forming programs are
divided into lexical rules and syntactic rules. The lexical rules, with only minor
variations, are the same for all the languages in the book:
• A semicolon starts a comment, which runs to the end of the line on which it
appears.
• Other characters are clumped into tokens that are as long as possible; a token
ends only at a bracket, a semicolon, or whitespace.
If you’re used to C or Java, these rules may surprise you in a one small way: inputs
like x+y and 3rd are single tokens—in Impcore, each is a valid name!
In this book, syntactic rules—that is, grammars—are written using Extended
Backus‐Naur Form, usually abbreviated as EBNF. EBNF is based on plain BNF,
which is ubiquitous. BNF has been extended in many different ways, and the EBNF
I use is best understood through an example: the grammar for Impcore, which ap‐
pears in Figure 1.1 on the next page. (ENBF is explained more fully in Appendix A.)
Figure 1.1, like any other grammar, lists nonterminal symbols like def , unittest,
exp, and so on. Each nonterminal is followed by the ::= symbol (pronounced “pro‐
duces”), followed by the forms of the phrases that the nonterminal canproduce.
Alternative forms are separated by vertical bars, as one thing another . In each
def ::= (val variablename exp)
| exp
| (define functionname (formals) exp)
| (use filename)
1 | unittest
unittest ::= (check‑expect exp exp)
| (check‑assert exp)
| (check‑error exp)
An imperative core
exp ::= literal
18 | variablename
| (set variablename exp)
| (if exp exp exp)
| (while exp exp)
| (begin exp )
| (functionname exp )
formals ::= variablename
literal ::= numeral
numeral ::= token composed only of digits, possibly prefixed with a plus
or minus sign
any *name ::= token that is not a bracket, a numeral, or one of the “re‐
served” words shown in typewriter font
Any syntactic form that is written with a matching pair of round brackets ( · · · )
may equally well be written with a matching pair of square brackets [ · · · ]. They
mean the same.
syntactic form, a token that is supposed to appear literally (like val or while) is
written in typewriter font; a name that stands for a token or for a sequence of
tokens (like def , variablename, or exp) is written in italic font. Finally, a phrase
that can be repeated is written in curly brackets, like the exp in the begin form;
a begin may contain any number of exps, including zero.
Figure 1.1 confirms that Impcore’s concrete syntax is fully parenthesized; wher‐
ever a sequence of tokens appears, that sequence is wrapped in brackets. You may
find this syntax unattractive, especially in complex expressions. But when you’re
learning multiple languages, it’s great not to have to worry about operator prece‐
dence. And when you must write a deeply nested expression with a ton of brackets,
you can reveal its structure by mixing round and square brackets—as long as round
matches round and square matches square, the two shapes are interchangeable.
Figure 1.1 begins with definitions. The definition form (val x e) defines a new
global variable x and initializes it to the value of the expression e. A global variable
must be defined before it is used or assigned to. Next, any expression exp may be
used as a definition form; it defines or assigns to the global variable it. And the def‐
inition form (define f (x1 · · · xn ) e) defines a function f with formal parameters
x1 to xn and body e.
The val, exp, and define forms are what I call true definitions; these forms
should be thought of as part of a program. The remaining forms, which I call ex
tended definitions, are more like instructions to the interpreter. The (use filename)
form tells the interpreter to read and evaluate the definitions in the named file.
A check‑expect, check‑assert, or check‑error form tells the interpreter to re‐
member a test and to run it after reading the file in which the test appears.
The expression forms, which all appear in the example while expression at the
beginning of the chapter, constitute a bare minimum needed for writing impera‐
tive or procedural code. The forms can express a literal value, a variable, an assign‐
ment (set), a conditional (if), a loop (while), a sequence (begin), and a function
§1.2
application (any other bracketed form).
The Impcore
Variables and functions are named according to liberal rules: almost any non‐
language
bracket token can be a name. Only the words val, define, use, check‑expect,
check‑assert, check‑error, set, if, while, and begin, are reserved—they cannot 19
be used to name functions or variables. And a numeral always stands for a number;
a numeral cannot be used to name a function or a variable.
When Impcore starts, some names are already defined. These include prim
itive functions +, ‑, *, /, =, <, >, println, print, and printu, and also predefined
functions and, or, not, <=, >=, !=, mod, and negated. A set of defined names forms
a basis; the set of names defined at startup forms the initial basis. The concepts of
primitive, predefined, and basis are explained in Section 1.2.6 (page 26).
Once we know how code is formed, we can talk about what happens when it is run.
To talk about any well‐formed code, not just particular codes, we use names called
metavariables. In this chapter, the metavariables used to talk about code are as fol‐
lows:
e Any expression
d Any definition
n Any numeral
x Any name that is meant to refer to a variable or a parameter
f Any name that is meant to refer to a function
To talk about more than one expression or name, we use subscripts. For example,
we write an if expression as (if e1 e2 e3 ). Because each subexpression might be
different from the other two, each one is referred to by its own metavariable.
Metavariables are distinct from program variables. Program variables appear in
source code; metavariables stand for source code. Metavariables are written in
math italics and program variables in typewriter font. For example, x is a pro‐
gram variable: it is a name that can appear in source code. But x is a metavariable:
it stands for any name that could appear in source code. Metavariable x might
stand for x, but it might also stand for y, z, i, j, or any other program variable.
To illustrate the difference, when we write (val x 3), we mean the definition of
global program variable x. But when we write (val x e), we mean a template that
can stand for any definition of any global variable.
Metavariables differ from program variables in one other crucial respect:
a metavariable can’t be renamed without changing its meaning. So although you
can write (define double (n) (+ n n)) and it means exactly the same thing as
(define double (x) (+ x x)), you cannot write (val m g ) and have it mean the
same thing as (val x e): (val x e) is a template for an expression, but because
m and g mean nothing when used as metavariables, (val m g ) is gibberish. If you
want a distinct name for a new metavariable, the most you can do is decorate the
original name in some way—traditionally with a prime or a subscript. For example,
(val x′ e4 ) is also a template for an expression, and it’s an expression that might
be different from (val x e).
• To evaluate a literal expression, which in Impcore takes the form of
a numeral n, we return the 32‐bit integer that n stands for.
An imperative core • To evaluate an expression of the form (set x e), we evaluate e, as‐
sign its value to the variable x, and return the value. Variable x must
20 be a global variable or a formal parameter.
v Any value
In Impcore, all values are integers. Evaluating an expression produces a value and
may also have a side effect; in Impcore, possible side effects include printing some‐
thing or changing the value of a variable.
A literal 3 evaluates to the value 3.
21a. htranscript 12ai+≡ ◁ 12c 21b ▷
‑> 3
3
The global variable n defined earlier in the chapter evaluates to its value:
21b. htranscript 12ai+≡ ◁ 21a 21c ▷
‑> n §1.2
1 The Impcore
A set changes a variable’s value: language
21c. htranscript 12ai+≡ ◁ 21b 21d ▷ 21
‑> (set n ‑13)
‑13
‑> n
‑13
A loop’s value is always 0, but its evaluation can change the values of vari‐
ables:
21e. htranscript 12ai+≡ ◁ 21d 21f ▷
‑> (while (< n 0) (set n (+ n 10)))
0
‑> n
7
Expressions are evaluated according to the rules in Figure 1.2(a), and corre‐
sponding examples appear in Figure 1.2(b)—except for calls and names, because
examples of calls and names won’t fit in the figure. To illustrate calls and names,
negated 27c
I show what the Impcore interpreter does with two function definitions and calls. printu B
After evaluating a function’s definition, the interpreter echoes the function’s name.
21g. htranscript 12ai+≡ ◁ 21f 22a ▷
‑> (define add1 (n) (+ n 1))
add1
‑> (define double (n) (+ n n))
double
‑> (add1 4)
5
‑> (double (+ 3 4))
14
A user‐defined function is called much as in C: first the arguments are evaluated;
their values are the actual parameters. Then the function’s body is evaluated with
each actual parameter “bound to” (which is to say, named by) the corresponding
formal parameter from the formals in the function’s definition. In the first example,
1 the actual parameter is 4, and (+ n 1) is evaluated with 4 bound to n. In the second,
the actual parameter is 7, and (+ n n) is evaluated with 7 bound to n.
In Impcore, a function call behaves nicely only if the number of actual param‐
eters is exactly equal to the number of formal parameters in the function’s defini‐
tion. Otherwise, the call goes wrong:
An imperative core
22a. htranscript 12ai+≡ ◁ 21g 22b ▷
22 ‑> (add1 17 12)
Run‑time error: in (add1 17 12), expected 1 argument but found 2
Within the body of addn, the two occurrences of n refer to the formal parameter. But
in the top‐level expressions n and (addn n 1), n refers to the global variable. And in
the body of addn, where n is set, changing the formal n does not affect anything in
the calling context; we say that Impcore passes parameters by value. No assignment
to a formal parameter ever changes the value of a global variable.
With the details of expressions explored, we turn our attention to definitions.
When a definition is evaluated, no value is returned; instead, evaluating a definition
updates some part of the interpreter’s state, causing it to “remember” something.
The Impcore interpreter can remember global variables, function definitions, and
pending unit tests.
• To evaluate a definition of the form (val x e), we first check to see if a global
variable named x exists, and if not, we create one. We then evaluate e and
assign its value to x.
• To evaluate a definition of the form (define f (x1 · · · xn ) e), like the defi‐
nitions of add1 or double, we remember f as a function that takes arguments
x1 , . . . , xn and returns e.
• To evaluate a definition of the form (use filename), we look for a file called
filename, which should contain a sequence of Impcore definitions. We read
the definitions and evaluate them in order. And after reading the file, we run
any unit tests it contains.
Not every function is defined using define; some are built into the interpreter as
primitives. Each primitive function takes two arguments, except the printing primi‐
tives, which take one each. The arithmetic primitives +, ‑, *, and / do arithmetic on
integers, up to the limits imposed by a 32‐bit representation. Each of the compari‐
son primitives <, >, and = does a comparison: if the comparison is true, the primi‐
tive returns 1; otherwise, it returns 0.
The printing primitives demand detailed explanation. Primitive println prints
a value and then a newline; it’s the printing primitive you’ll use most often. Prim‐
itive print prints a value and no newline. Primitive printu prints a Unicode char‐
acter and no newline. More precisely, printu takes as its argument an integer that
stands for a Unicode code point—that means it’s an integer code that stands for a
character in one of a huge variety of alphabets. Primitive printu then prints the
UTF‐8 byte sequence that represents the code point. In most programming envi‐
ronments, this sequence will give you the character you’re looking for. For exam‐
ple, (printu 955) prints the Greek letter λ. Each printing primitive, in addition to
its side effect, also returns its argument.
When used interactively, the printing primitives can be confusing, because
whenever an expression is evaluated, its value is printed automatically by the in‐
terpreter. Don’t be baffled by effects like these:
23a. htranscript 12ai+≡ ◁ 22b 23b ▷
‑> (val x 4)
‑> (println x)
4
4
add1 21g
The 4 is printed twice because println is called with actual parameter 4 (the value
of x), and println first prints 4, accounting for the first 4, then returns 4. The sec‐
ond 4 is printed because the interpreter prints the value of every expression, in‐
cluding (println x).
23b. htranscript 12ai+≡ ◁ 23a 24a ▷
‑> (val y 5)
5
‑> (begin (println x) (println y) (* x y))
4
5
20
True definitions and extended definitions: A design compromise
If you want to learn to use programming languages well and also to describe
1 them precisely, what languages should you study? Not big industrial languages—
you can write interesting programs, but it’s hard to say how programs behave,
or even what programs are well behaved. And not a tiny artificial language (or a
“core calculus” like the famous lambda calculus)—its behavior can be described
very precisely, but it’s hard to write any interesting programs. That’s why I’ve
An imperative core designed the bridge languages: to bridge the gap between industrial languages
and core calculi.
24
The bridge languages are small enough to be described precisely, but big
enough for interesting programs. (I won’t pretend you can write interesting pro‐
grams in Impcore, but you can write interesting programs in µScheme, µML,
Molecule, and µSmalltalk.) But a few features are too complicated to define
precisely, yet too useful to leave out. They are the “extended definitions”: the
use, check‑expect, check‑assert, and check‑error forms. The val, define,
and top‐level expression forms, which are defined precisely, are the “true def‐
initions.” The true definitions are part of the language, and the extensions are
there to make you more productive as a programmer.
If you happen to use print instead of println, you can get some strange output:
24a. htranscript 12ai+≡ ◁ 23b 25b ▷
‑> (begin (print x) (print y) (* x y))
4520
Because the interpreter automatically prints the value of each expression you
enter, you’ll use printing primitives rarely—mostly for debugging. I typically debug
using println, but when I want fancier output, I also use printu and print.
The examples above show transcripts of my interactions with the Impcore inter‐
preter. But interactive code disappears as soon as it is typed; to help you write code
that you want to edit or keep, the Impcore interpreter, like all the interpreters in
this book, enables you to put it in a file. And when you put code in a file, you can
add unit tests. For example, the file gcd.imp tests a function that computes greatest
common denominators:
24b. hcontents of file gcd.imp 24bi≡ 25a ▷
(val r 0)
(define gcd (m n)
(begin
(while (!= (set r (mod m n)) 0)
(begin
(set m n)
(set n r)))
n))
Since the code is in a file, there are no arrow prompts. But there is a unit test,
our first: the check‑expect says that if we call (gcd 6 15), the result should be 3.
The file includes more unit tests:
25a. hcontents of file gcd.imp 24bi+≡ ◁ 24b
(check‑expect (gcd 15 15) 15)
(check‑expect (gcd 14 15) 1)
(check‑expect (gcd 14 1) 1)
(check‑expect (gcd 72 96) 24)
(check‑error (gcd 14 0))
§1.2
The last unit test says that if we evaluate (gcd 14 0), we expect a run‐time error.
The Impcore
Unit tests aren’t run until after a file is loaded. Then the interpreter summarizes
language
the test results:
25b. htranscript 12ai+≡ ◁ 24a 25d ▷ 25
‑> (use gcd.imp)
0
gcd
All 6 tests passed.
A unit test can appear before the function it tests. This trick can be a great way
to plan a function, or to document it. I’ve written an example using triangular num
bers. A triangular number is analogous to the square of a number: just as the square
of n is the number of dots needed to form a square array with a side of length n, the
nth triangular number is the number of dots needed to form an equilateral triangle
with n dots along one side.
1 = *
3 = *
* *
*
6 = * *
* * *
When writing triangle, I botched my first attempt. My unit tests caught the
botch. The botched code, preceded by tests, looked like this:
25e. hbotchedtriangle.imp 25ei≡
(check‑expect (triangle 1) 1)
(check‑expect (triangle 2) 3)
(check‑expect (triangle 3) 6)
(check‑expect (triangle 4) 10)
Function not is predefined; its definition appears in Figure 1.3 (page 27).
26c. htranscript 12ai+≡ ◁ 26a 29 ▷
‑> (use arith‑assertions.imp)
All 3 tests passed.
Programmers like big languages with lots of data types and syntactic forms, but
implementors want to keep primitive functionality small and simple. (So do se‐
manticists!) To reconcile these competing desires, language designers have found
two strategies: translation into a core language and definition of an initial basis.
Using a core language, you stratify your language into two layers. The inner
layer defines or implements its constructs directly; it constitutes the core language.
The outer layer defines additional constructs by translating them into the core lan‐
guage; these constructs constitute syntactic sugar. In this chapter, the core language
is Impcore, and as an example of syntactic sugar, a for expression can be defined
by a translation into begin and while (Section 1.8).
To be useful to programmers, a language needs to be accompanied by a stan‐
dard library. In the theory world, a library contributes to a basis; basis is the col‐
lective term for all the things that can be named in definitions. In Impcore, these
things are functions and global variables. A language’s initial basis contains the
named things that are available in a fresh interpreter or installation—the things
you have access to even before evaluating your own code.
Like the Impcore language, Impcore’s initial basis is stratified into two layers,
one of which is defined in terms of the other. The inner layer includes all the func‐
tions that are defined directly by C code in the interpreter; these are called primi
tive. The outer layer includes functions that are also built into the interpreter, but
are defined in terms of the primitives using Impcore source code; they are user‐
defined functions and are called predefined.
Stratifying the initial basis makes life easy for everyone. Implementors make
their own lives easy by defining just a few primitives, and they can make program‐
mers’ lives easy by defining lots of predefined functions. Predefined functions are
Boolean connectives are defined using if expressions.
27a. hpredefined Impcore functions 27ai≡ 27b ▷
(define and (b c) (if b c b))
(define or (b c) (if b b c))
(define not (b) (if b 0 1))
Unlike the similar constructs built into the syntax of many languages, these ver‐
sions of and and or always evaluate both of their arguments. Section G.7 shows
how you can use syntactic sugar to define shortcircuit variations that evaluate a §1.3
second expression only when necessary. Abstract syntax
Only comparisons <, =, and > are primitive; the others are predefined.
27
27b. hpredefined Impcore functions 27ai+≡ ◁ 27a 27c ▷
(define <= (x y) (not (> x y)))
(define >= (x y) (not (< x y)))
(define != (x y) (not (= x y)))
These functions are installed into the initial basis by the C code in chunk hinstall
the initial basis in functions S297bi, which is continued in chunk S297d.
just ordinary code, and writing them is lots easier than defining new primitives.
Impcore’s predefined functions are defined by the code in Figure 1.3.
Just like any other function, a primitive or predefined function can be redefined
using define. This trick can be useful—for example, to count the number of times
a function is called. But if you redefine an initial‐basis function, don’t change the
results it returns! (You’ll introduce bugs.)
Before going on to the next sections, work some of the exercises in Sec‐
tions 1.10.2 to 1.10.4, starting on page 73.
1 | VAR
| SET
(Name)
(Name, Exp)
| IF (Exp, Exp, Exp)
| WHILE (Exp, Exp)
| BEGIN (Explist)
An imperative core | APPLY (Name, Explist)
28 This description is worth comparing this with the description of exp in the grammar
for Impcore (Figure 1.1, page 18).
An abstract‐syntax tree for a given form is usually drawn with the form’s name
at the root and with its subtrees connected to it by solid lines. When a form has
no subtrees, like LITERAL or VAR, its single node can be drawn with the associated
Value or Name just underneath. For example, the abstract‐syntax tree for the Imp‐
core expression (set i (‑ (+ (* 2 j) i) (/ k 3))) can be drawn like this:
SET
i APPLY
‑ Explist
APPLY APPLY
+ Explist / Explist
APPLY VAR VAR LITERAL
* Explist i k 3
LITERAL VAR
2 j
An abstract‐syntax tree is much easier to analyze, manipulate, or interpret than
source code. And ASTs focus our attention on structure and semantics. Concrete
syntax becomes a separate concern; one could easily define a version of Impcore
with C‐like concrete syntax, but with identical abstract syntax.
Abstract syntax is created from concrete input by a parser, which also identifies
and rejects ill‐formed phrases such as (if x 0) or (val y). Parsing is covered in
a large body of literature; for textbook treatments, try Appel (1998) or Aho et al.
(2007). A parser for Impcore, which you can easily extend, can be found in Ap‐
pendix G.
Environments ξ and ϕ are global and shared, but there is a distinct ρ for every func‐
tion call. Together, the contents of the three environments comprise Impcore’s
basis. §1.5
A name can be defined in all three environments at once. But it’s a bad idea: Operational
29. htranscript 12ai+≡ ◁ 26c 64 ▷ semantics
‑> (val x 2)
29
2
‑> (define x (y) (+ x y)) ; pushing the boundaries of knowledge...
‑> (define z (x) (x x)) ; and sanity
‑> (z 4)
6
The val definition introduces a global variable x, which is bound to value 2 in en‐
vironment ξ . The first define introduces a function x that adds its argument to
the global variable x; that function is bound to name x in environment ϕ. The sec‐
ond define introduces function z, which passes its formal parameter x to the func‐
tion x; when z is called, parameter x is bound to 4 in environment ρ. This example
should push you to understand the rules of Impcore; to follow it, you have to know
not only that Impcore has three environments but also how the environments are
used. Of course, no sane person programs this way; production code is written to
be easy to understand, even by readers who may have forgotten details of the rules.
Mathematically, an environment is a function with a finite domain, from
a name to whatever. In Impcore, environments ξ and ρ map each defined name to a
value; ϕ maps each defined name to a function. Whatever a name is mapped to, all
environments are manipulated using the same notation, which is mostly function
notation. For example, whatever is associated with name x in the environment ρ
is written ρ(x). The set of names bound in environment ρ is written dom ρ. An ex‐
tended environment, ρ plus a binding of the name x to v , is written ρ{x 7→ v}.
In an extended environment, the new binding hides previous bindings of x, so
lookup is governed by this equation:
v, when y = x
ρ{x 7→ v}(y) =
6 x.
ρ(y), when y =
Finally, an empty environment, which does not bind any names, is written {}.
One environment can be combined with another, but because combining en‐
vironments is not useful in Impcore, the notation is deferred to Chapter 2.
e, ei An expression
d A definition
1 x, xi
f
A name that refers to a variable or a parameter
A name that refers to a function
v, vi A value
ξ, ξ ′ , . . . A global‐variable environment
An imperative core
ϕ, ϕ′ , . . . A function‐definition environment
ρ, ρ′ , . . . A formal‐parameter environment
30
State transitions are described using judgments, which take one form for defini‐
tions and another form for expressions. The judgment for the evaluation of an
expression, he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i, means “evaluating expression e produces
value v .” More precisely, it means “in environments ξ , ϕ, and ρ, evaluating e pro‐
duces a value v , and it also produces new environments ξ ′ and ρ′ , while leaving ϕ
unchanged.”2 This judgment uses eight metavariables; e stands for an expression,
ξ , ϕ, and ρ stand for environments, and v stands for a value. Different metavaria‐
bles are distinguished by giving them subscripts, or as with the environments, by
using primes.
The form of the judgment he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i tells us a few things:
The form of the judgment doesn’t tell us whether evaluating an expression can in‐
troduce a new variable. (It can’t, but we can be certain of this only if we study the
full semantics and write an inductive proof, which is Exercise 24 on page 83.)
The form of the evaluation judgment also gives this semantics part of its name:
no matter how much computation is required to get from e to v , the judgment
he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i encompasses all that computation in one big step. It is
therefore called a bigstep judgment and is part of a bigstep semantics.
The judgment for a definition is simpler; hd, ξ, ϕi → hξ ′ , ϕ′ i means “evaluating
definition d in the environments ξ and ϕ yields new environments ξ ′ and ϕ′ .” The
different arrow helps distinguish this judgment from an expression judgment.
Not all judgments describe real program behaviors. For example, unless some
joker changes the binding of the name + in ϕ, h(+ 1 1), ξ, ϕ, ρi ⇓ h4, ξ, ϕ, ρi doesn’t
describe how Impcore code behaves. To say which judgments describe real behav‐
iors, an operational semantics uses rules of inference. Each rule has the form
premises
. (NAME OF RULE)
conclusion
If all the premises hold, so does the conclusion.
For example, the rule
. (LıTERAL)
hLıTERAL(v), ξ, ϕ, ρi ⇓ hv, ξ, ϕ, ρi
1.5.3 Variables
x ∈ dom ρ
(FORMALVAR)
hVAR(x), ξ, ϕ, ρi ⇓ hρ(x), ξ, ϕ, ρi
Our notation can imply only that two environments “may differ” or “must equal”
each other. For example, in the rule for IFTRUE on page 34, ξ may equal ξ ′ ,
which may equal ξ ′′ , as in (if (> n 0) n (‑ 0 n)). Or ξ may equal ξ ′ , but they
may both differ from ξ ′′ , as in (if (> n 0) (set sign 1) (set sign ‑1)).
What if ξ and ξ ′ must differ? That can’t be said with primes or subscripts; primes
and subscripts can say only “may differ” or “must equal.” To say that two things
must differ, write an explicit premise, like “v1 6= 0” or “ξ 6= ξ ′ .”
The notation is most important when you write your own derivations. Begin‐
ners often write primes or subscripts as they appear in rules. But a prime or
subscript says “I don’t know; it may differ,” and in a derivation, this is almost
always wrong—we do know. Here’s an invalid derivation, intended to describe
the evaluation of (if (> n 0) n (‑ 0 n)) when n is 7 (I’ve taken a minor liberty
with the notation):
··· ···
h(> n 0), ξ, ϕ, ρi ⇓ h1, ξ, ϕ, ρi 1= 6 0 hn, ξ, ϕ, ρi ⇓ h7, ξ, ϕ, ρi
.
hıF((> n 0), n, (‑ 0 n)), ξ, ϕ, ρi ⇓ h7, ξ ′′ , ϕ, ρ′′ i
The conclusion is bogus. Because ξ ′′ appears only in the conclusion, there is
nothing that ξ ′′ must equal, and that means the judgment says, “evaluating the
if expression produces 7, and afterward, global variables have arbitrary values.”
(The same goes for ρ′′ .) To avoid this problem, remember that in any judgment
that you prove, the elements of the final state must equal something that you
specify. Do that and you’ll write good derivations.
1.5.4 Assignment
Evaluating an assignment changes the value of a variable, and it produces the value
of the right‐hand side. Assignment, like variable lookup, prioritizes parameters:
34 x∈
/ dom ρ x ∈ dom ξ he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i
(GLOBALAſſıGN)
hſET(x, e), ξ, ϕ, ρi ⇓ hv, ξ ′ {x 7→ v}, ϕ, ρ′ i
Each of these rules has a premise that shows an evaluation of the right‐hand side e,
so at a ſET node, the recursive implementation of eval always makes a recursive
call. And even though there are two rules with ſET in the conclusion, only one can
apply at one time, because the premises x ∈ dom ρ and x ∈ / dom ρ are mutually
exclusive. This property enables the implementation to know exactly what to do
with a ſET node, and it keeps the evaluation of Impcore programs deterministic.
Because of the premises x ∈ dom ρ and x ∈ dom ξ , only a previously defined
variable may be assigned to; given a ſET node where x ∈ / dom ρ and x ∈ / dom ξ ,
the machine gets stuck. In many languages, like Awk for example, assignment to an
undefined and undeclared variable creates a new global variable (Aho, Kernighan,
and Weinberger 1988), as specified by the following rule:
In Impcore, a new global variable can be created only by a VAL definition, as shown
below in rule DEFıNEGLOBAL (page 37). To spot such subtleties, you have to read
inference rules carefully.
Conditional evaluation
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 = 6 0
he2 , ξ ′ , ϕ, ρ′ i ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i hWHıLE(e1 , e2 ), ξ ′′ , ϕ, ρ′′ i ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
§1.5
hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
(WHıLEITERATE) Operational
In this rule, the value v2 , which is produced by evaluating the body e2 , is thrown semantics
away. We can tell by looking at the final state of the judgment in the conclusion 35
of the rule. That state has elements v3 , ξ ′′′ , ϕ, and ρ′′′ , and if you study the rule
carefully, you’ll see that none of these elements depends on or uses v2 . A WHıLE
loop evaluates its body e2 only for its side effects, i.e., for the new environments
ξ ′′ and ρ′′ . They are then used to make the final environments ξ ′′′ and ρ′′′ .
If the condition in a WHıLE loop evaluates to zero, the loop terminates, and the
loop also evaluates to zero.
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 = 0
(WHıLEEND)
hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ h0, ξ , ϕ, ρ′ i
′
If the evaluation of a WHıLE loop terminates, it always produces zero, even when
rule WHıLEITERATE is used (Exercise 23 on page 83). A WHıLE loop is therefore
executed for its side effects.
Sequential execution
A nonempty BEGıN evaluates its subexpressions left to right, producing the result
of the final expression en .
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
..
.
hen , ξn−1 , ϕ, ρn−1 i ⇓ hvn , ξn , ϕ, ρn i
(BEGıN)
hBEGıN(e1 , e2 , . . . , en ), ξ0 , ϕ, ρ0 i ⇓ hvn , ξn , ϕ, ρn i
Values v1 to vn−1 are ignored, but the environments ξ1 and ρ1 , which result from
evaluating e1 , are used to evaluate e2 , and so on. The use of ξ1 and ρ1 to evaluate e2
implies that e1 is evaluated before e2 . Order of evaluation is determined by this
“threading” of environments, not by the order in which the premises are written.
For example, the BEGıN rule might equally well have been written this way:
This equivalent rule still specifies that e1 is evaluated before e2 , and so on, but when
the rule is written this way, it is not as easy to understand.
A BEGıN might be empty, in which case it evaluates to zero.
(EMPTYBEGıN)
hBEGıN(), ξ, ϕ, ρi ⇓ h0, ξ, ϕ, ρi
1.5.6 Function application
In the rules for function application, operational semantics first starts to show its
advantages: the description is precise, and it is much more concise than an imple‐
1 mentation.
Userdefined functions
As in the BEGıN rule, expressions e1 through en are evaluated in order. Their values
v1 to vn are used to create a new, unnamed formal‐parameter environment that
maps each formal parameter xi to the corresponding vi . This environment is used
as a ρ to evaluate e, the body of the function.
The rule has these implications:
• The behavior of a function doesn’t depend on the function’s name, but only
on the definition to which the name is bound.
• The body of a function can’t get at the formal parameters of its caller, since
the body e is evaluated in a state that does not contain ρ0 , . . . , ρn .
• If a function assigns to its own formal parameters, its caller can’t get at the
new values, because the caller has no access to the environment ρ′ .
Primitive functions
ϕ(f ) = PRıMıTıVE(+)
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
− 231 ≤ v1 + v2 < 231
. (APPLYADD)
hAPPLY(f, e1 , e2 ), ξ0 , ϕ, ρ0 i ⇓ hv1 + v2 , ξ2 , ϕ, ρ2 i §1.5
Operational
The final condition on the sum v1 + v2 ensures that the result can be represented
semantics
in a 32‐bit signed integer.
Comparison primitives are represented by equality. Like the if expression, 37
the equality primitive is specified by a pair of rules which have mutually exclusive
premises (v1 = v2 and v1 6= v2 ):
ϕ(f ) = PRıMıTıVE(=)
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
v1 = v2 , (APPLYEQTRUE)
hAPPLY(f, e1 , e2 ), ξ0 , ϕ, ρ0 i ⇓ h1, ξ2 , ϕ, ρ2 i
ϕ(f ) = PRıMıTıVE(=)
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
v1 6= v2 . (APPLYEQFALſE)
hAPPLY(f, e1 , e2 ), ξ0 , ϕ, ρ0 i ⇓ h0, ξ2 , ϕ, ρ2 i
The printing primitives are represented by println. These primitives have an
important behavior that can’t be expressed by our formal evaluation judgment:
they print. Because this behavior is omitted, the rule makes println look like the
identity function.
ϕ(f ) = PRıMıTıVE(println)
he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i
(APPLYPRıNTLN)
hAPPLY(f, e), ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i while printing v
The rules above specify the effects of evaluating expressions. The effects of eval‐
uating definitions are different: evaluating a definition does not produce a value,
but unlike an expression, it may introduce a new global variable or a new function.
New variables are added to ξ and new functions are added to ϕ, so the evaluation
of a definition is described by a judgment of the form hd, ξ, ϕi → hξ ′ , ϕ′ i. Below,
this judgment is used in rules for true definitions, which are implemented by the
evaldef function in Section 1.6.2. Extended definitions aren’t formalized.
Variable definition
Function definition
An imperative core
The definition DEFıNE(f, hx1 , . . . , xn i, e) introduces a function f . The function
38
is represented as UſER(hx1 , . . . , xn i, e), where x1 , . . . , xn are the names of the
formal parameters and e is the body. No two parameters may have the same name.
x1 , . . . , xn all distinct
hDEFıNE(f, hx1 , . . . , xn i, e), ξ, ϕi → hξ, ϕ{f 7→ UſER(hx1 , . . . , xn i, e)}i
(DEFıNEFUNCTıON)
Toplevel expression
Extended definitions
A bridge language like Impcore is meant to be small enough to learn, small enough
to specify, small enough to implement, and yet big enough to write interesting pro‐
grams in. (Or in the case of Impcore, not quite big enough.) To write programs
requires an implementation, but why talk about it? Why not bury the implementa‐
tion in a repository somewhere? Because the implementation of an interpreter can
illustrate a language and its semantics in a way that nothing else can. And when you
want to experiment with alternative language designs, you can build on or change
my code. To make such experiments possible, I can’t just hand you the code; I have
to explain it. But I can’t explain all of it—the explanations would add over 300 pages
to this book. In this chapter, I explain just the most important parts.
A language is embodied by the data structures for its crucial abstractions (envi‐
§1.6
ronments and abstract‐syntax trees) and by the functions that evaluate expressions
The interpreter
and definitions. They are all explained in this chapter. The crucial code is built on
top of infrastructure: error handling, parsing, printing, test reporting, and so on. 39
That infrastructure is lovingly described in a Supplement to this book, which is
available from build‑prove‑compare.net. The interpreter requires only a stan‐
dard C library, and with that and the Supplement, you can understand as much or
as little as you wish.
The code is presented using the Noweb system for literate programming. Noweb
extracts code directly from the text, so the code in the book is the code that runs.
Noweb splits code into named “code chunks,” which are surrounded by textual ex‐
planations. The code chunks are written in an order designed to support good ex‐
planations, not the order dictated by a C compiler.
Code chunks can mix source code with references to other chunks. References
are italicized in angle brackets, as in hevaluate e‑>ifx and return the result 49ci.
The label “49c” shows where to find the definition: the number identifies a page,
and when the page contains more than one chunk, each chunk gets its own lower‐
case letter, which is appended to the page number. The label also appears on the
first line of the definition, in bold. Each chunk definition is shown using the ≡ sign.
A definition can be continued in a later chunk; Noweb concatenates the contents of
all definitions of the same chunk. A definition that continues a previous definition
is showing using the +≡ sign in place of the ≡ sign. When a chunk’s definition is
continued, the right margin displays pointers to the previous and next definitions,
written “◁ 48a” and “S296a ▷.” The notation “(48b)” shows where a chunk is used.
To help you find relevant chunks, Noweb provides a miniindex in the margin of
each right‐hand page. For example, the mini‐index on page 49 reveals that function
bindval is defined in chunk 45b on page 45. It also reveals that type Exp is not
defined by hand‐written code; it is generated automatically. In any mini‐index,
A stands for automatically generated code, and B stands for a basis function from
C’s standard library. And P , which is used from Chapter 2 onward, stands for a
primitive function that is defined in an interpreter.
In most chapters, Noweb’s information is supplemented by a table that relates
semantics, concepts, and code, like Table 1.5 on the next page. This table will help
you learn the important parts of the code, which all relate to metavariables and
math symbols from Section 1.5.
The code will be clearer to you if you know my programming conventions. For
example, when I introduce a new type, I use typedef to give it a name that begins
with a capital letter, like Name, or Exp, or Def. The representation of such a type is
often exposed, in which case you get to see all of the type’s definition, and you get
access to fields of structures and so on. A type whose representation is exposed is
called manifest; for example, types Exp and Def are manifest. A type might also be
abstract, in which case you can’t get at its representation. In C, an abstract type is
always a pointer to a named struct whose fields are not specified. For example,
type Name is abstract; you can store a Name in a field or a variable, and you can pass
a Name to a function, but you can’t look inside a Name to see how it is represented.
(Strictly speaking, you can see everything; as explained in Chapter 9, it is your client
code, and mine, that cannot see the representations of abstract types.)
Table 1.5: Correspondence between Impcore semantics and code
1 d
e
True definition
Expression
Def (page 42)
Exp (page 42)
x, f Name Name (page 43)
I write the names of functions using lowercase letters only, except for some
automatically generated functions used to build lists.
To the degree that C permits, I distinguish interfaces from implementations.
An interface typically includes some or all of these elements:
A language in which all errors are checked is called safe. Safety is usually imple‐
mented by a combination of compile‐time and run‐time checking. Popular safe
languages include Awk, C#, Haskell, Go, Java, JavaScript, Lua, ML, Perl, Python,
Ruby, Rust, Scheme, and Smalltalk. A safe language might be characterized by
saying that “there are no unexplained core dumps”; a program that halts always
issues an informative error message. §1.6
The interpreter
A language that permits unchecked errors is called unsafe. Unsafe languages
put an extra burden on the programmer, but they provide extra expressive 41
power. This extra power is needed to write things like garbage collectors and
device drivers; systems programming languages, like Bliss and C, have histor‐
ically been unsafe. C++ is an anomaly: it is ostensibly intended for high‐level
problem‐solving, but it is nevertheless unsafe.
A few well‐designed systems‐programming languages are safe by default,
but have unsafe features that can be turned on explicitly at need, usually by
a keyword UNSAFE. The best known of these may be Cedar and Modula‐3.
cial implementations, of functions like eval and evaldef, are intended for you to
look at. When you look at them, you’ll see that they respect these conventions:
• Within reason, each local variable is declared in the region in which it is used;
local variables typically don’t scope over an entire function definition.
On to the code! The presentation begins with the interfaces in Section 1.6.1.
These interfaces include not only the interfaces associated with the evaluator, but
also some interfaces associated with general‐purpose utility code from the Supple‐
ment (Appendix F). These interfaces are necessary building blocks, but they aren’t
the main event; the main event is the implementation of Impcore’s operational se‐
mantics in Section 1.6.2, which starts on page 48.
1.6.1 Interfaces
Everything comes together in the evaluator, which implements the operational se‐
mantics. That evaluator is supported by interfaces for the central structures of a
programming language: syntax, names, values, environments, and lists thereof.
It is also supported by interfaces used for printing and for reporting errors.
Such type definitions and creator functions are hard to write and maintain by
hand. So I generate them automatically, using an ML program that appears in Ap‐
pendix J. That program generates the Def type and associated functions from the
following descriptions:
42b. hdefinition.t 42bi≡
Userfun = (Namelist formals, Exp body)
Def* = VAL (Name name, Exp exp)
| EXP (Exp)
| DEFINE (Name name, Userfun userfun)
A valid Userfun satisfies the invariant that the names in formals are all distinct.
The abstract syntax for Exp, which you might wish to compare with the concrete
syntax given for exp on page 18, is described as follows:
42c. hexp.t 42ci≡
Exp* = LITERAL (Value)
| VAR (Name)
| SET (Name name, Exp exp)
| IFX (Exp cond, Exp truex, Exp falsex)
| WHILEX (Exp cond, Exp exp)
| BEGIN (Explist)
| APPLY (Name name, Explist actuals)
The descriptions above4 are slightly elaborated versions of hsimplified example
of abstract syntax for Impcore 27di. Similar descriptions are used for much of the
C code in this book.
True definitions and expressions are the essential elements of abstract syntax;
once you understand how they work, you will be ready to connect the operational
semantics and the code. Impcore’s extended definitions, including unit tests, are
described in the Supplement.
§1.6
Interface to names: An abstract type The interpreter
Programs are full of names. To make it easy to compare names and look them up 43
in tables, I define an abstract type to represent them. Although each name is built
from a string, the abstract type hides the string and its characters. Unlike C strings,
names are immutable, and two names are equal if and only if they are the same
pointer.
43a. hshared type definitions 43ai≡ (S295a)
typedef struct Name *Name;
typedef struct Namelist *Namelist; // list of Name
strcmp(s, nametostr(strtoname(s))) == 0
strcmp(s, t) == 0 if and only if strtoname(s) == strtoname(t)
The first law says if you build a name from a string, nametostr returns a copy of
your original string. The second law says you can compare names using pointer
equality.
Because nametostr returns a string of type const char*, a client of nametostr
cannot modify that string without subverting the type system. Modification of the
string is an unchecked run‐time error. New values of type Name* should be created
only by calling strtoname; to do so by casting other pointers is a subversion of the
type system and an unchecked run‐time error.
Interface to values
The value interface defines the type of value that an expression may evaluate to.
In Impcore, that is always a 32‐bit integer. A Valuelist is a list of Values.
43c. htype definitions for Impcore 43ci≡ (S295a) 44a ▷
typedef int32_t Value;
typedef struct Valuelist *Valuelist; // list of Value
4
The alternatives for if and while are named IFX and WHILEX, not IF and WHILE. Why? Because
corresponding to each alternative, there is a field of a union that uses the same name in lower case.
For example, if e is a LITERAL expression, the literal Value is found in field e‑>literal. But a structure
field can’t be named if or while, because the names if and while are reserved words—they may be used
only to mark C syntax. So I call these alternatives IFX and WHILEX, which I encourage you to think of
as “if‐expression” and “while‐expression.” For similar reasons, the two branches of the IFX are called
truex and falsex, not true and false. And in Chapter 2, you’ll see LETX and LAMBDAX instead of LET
and LAMBDA, so that I can write an interpreter for µScheme in µScheme.
Interface to functions, both userdefined and primitive
In the Impcore interpreter, the type “function” is another sum type. This type
specifies two alternatives: user‐defined functions and primitive functions. Fol‐
In the operational semantics, the environments ρ and ξ hold values, and the envi‐
ronment ϕ holds functions. Each kind of environment has its own representation.5
44e. htype definitions for Impcore 43ci+≡ (S295a) ◁ 44a
typedef struct Valenv *Valenv;
typedef struct Funenv *Funenv;
A new environment may be created by passing a list of names and a list of asso‐
ciated values or function definitions to mkValenv or mkFunenv. For example, calling
mkValenv(hx1 , . . . , xn i, hv1 , . . . , vn i) returns {x1 7→ v1 , . . . , xn 7→ vn }. Passing
lists of different lengths is a checked run‐time error.
44f. hfunction prototypes for Impcore 44di+≡ (S295a) ◁ 44d 44g ▷
Valenv mkValenv(Namelist vars, Valuelist vals);
Funenv mkFunenv(Namelist vars, Funclist defs);
To add new bindings to an environment, use bindval and bindfun. Unlike the pre‐ §1.6
vious six functions, bindval and bindfun are not pure: instead of returning new The interpreter
environments, bindval and bindfun mutate their argument environments, replac‐
ing the old bindings with new ones. Calling bindval(x, v , ρ) is equivalent to per‐ 45
forming the assignment ρ := ρ{x 7→ v}. Because ρ is a mutable abstraction, mod‐
ifications to the environment are visible to whatever code calls bindval.
45b. hfunction prototypes for Impcore 44di+≡ (S295a) ◁ 45a 45c ▷
void bindval(Name name, Value val, Valenv env);
void bindfun(Name name, Func fun, Funenv env);
These functions can be used to replace existing bindings or to add new ones.
The evaluator works with abstract syntax and values, whose representations are
exposed, and with names and environments, whose representations are not ex‐
posed. Its interface exports functions eval and evaldef, which evaluate expres‐
sions and true definitions, respectively. (Extended definitions are evaluated by
function readevalprint, which is described in the Supplement.) Function eval im‐
plements the ⇓ relation in our operational semantics. For example, eval(e, ξ, ϕ, ρ)
finds a v , ξ ′ , and ρ′ such that he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i, assigns ρ := ρ′ and ξ := ξ ′ ,
and returns v . Function evaldef similarly implements the → relation.
45c. hfunction prototypes for Impcore 44di+≡ (S295a) ◁ 45b
Value eval (Exp e, Valenv globals, Funenv functions, Valenv formals);
void evaldef(Def d, Valenv globals, Funenv functions, Echo echo_level);
Just as the forms of the evaluation judgments tell us something about the oper‐
ational semantics, the types of the evaluation functions tell us something about
the implementation. The result types confirm that evaluating an Exp produces a type Def A
value but evaluating a Def does not. Both kinds of evaluations can have side effects type Echo S293f
on environments. Finally, the echo_level parameter, which has no counterpart type Exp A
in the semantics, controls printing: when echo_level is ECHOING, evaldef prints type Func A
type Name 43a
the values and names of top‐level expressions and functions. When echo_level is type Namelist
NOT_ECHOING, evaldef does not print. 43a
type Userfun A
type Value 43c
Interface to lists
type Valuelist
43c
The evaluator’s data structures include many lists: names, values, functions, ex‐
pressions, and unit tests are all placed in lists. For safety, each list has its own
type: Namelist, Valuelist, Funclist, Explist, and UnitTestlist. Each of these
types is recursive; a list is either empty or is a pointer to a pair (hd, tl), where hd is
the first element of the list and tl is the rest of the list. An empty list is represented
by a null pointer. Each list type is defined in the same way, as in this example:
45d. hexample structure definitions for Impcore 45di≡
struct Explist {
Exp hd;
struct Explist *tl;
};
The type definitions are generated by a Lua script, which searches header files
for lines of the form
1 For each type of list, the script also generates a length function, an extractor, a cre‐
ator function, and a print function. These functions are named lengthTL, nthTL,
mkTL, and printTL, where T is the first letter of the list type. The length of the NULL
list is zero; the length of any other list is the number of its elements. Elements are
An imperative core numbered from zero, and asking for nthTL(xs, n) when n ≥ lengthTL(xs) is a
checked run‐time error. Calling mkTL creates a fresh list with the new element at
46 the head; it does not mutate the old list.
The list functions have prototypes like these:
46a. hexample function prototypes for Impcore 46ai≡
int lengthEL(Explist es);
Exp nthEL (Explist es, unsigned n);
Explist mkEL (Exp e, Explist es);
Explist popEL (Explist es);
Definitions and function prototypes for all the list types can be found in the in‐
terpreter’s all.h file. Because of the repetition, this code is tedious to read, but
generating the code automatically makes the tedium bearable. And ML’s polymor‐
phism enables a simpler solution (Chapter 5).
After evaluating a definition, the interpreter prints a name or a value. And when
an error occurs, the interpreter may need to print a faulty expression or definition.
Strings and numbers can easily be printed using printf, but expressions and def‐
initions can’t. So instead, the interpreter uses functions print and fprint, which
replace printf and fprintf. These functions, which are defined in the Supple‐
ment, support direct printing of Exps, Defs, Names, and so on.
46b. hshared function prototypes 43bi+≡ (S295a) ◁ 43b 47a ▷
void print (const char *fmt, ...); // print to standard output
void fprint(FILE *output, const char *fmt, ...); // print to given file
By design, print and fprint resemble printf and fprintf: the fmt parame‐
ter is a “format string” that contains “conversion specifications.” Our conversion
specifications are like those used by printf, but much simpler. A conversion spec‐
ification is two characters: a percent sign followed by a character like d or s, which
is called a conversion specifier. Unlike standard conversion specifications, ours don’t
contain minus signs, numbers, or dots. The ones used in the Impcore interpreter
are shown here in Table 1.6. By convention, lowercase specifiers print individual
values; uppercase specifiers print lists. Most specifiers are named for the initial let‐
ter of what they print, but the specifier for a Def must not be %d: the %d is too firmly
established as a specifier for printing decimal integers. Instead, Def is specified
by %t, for “top level,” which is where a Def appears.
Functions print and fprint are unsafe; if you pass an argument that is not consis‐
tent with the corresponding conversion specifier, it is an unchecked run‐time error.
When it encounters a fault, the Impcore interpreter complains and recovers by call‐
ing a function in an error‐handling interface. In general, a fault occurs whenever
Table 1.6: Conversion specifiers for impcore
a program is ill formed, ill typed, or ill behaved, but Impcore has no static type
system, so faults are triggered only by ill‐formed and ill‐behaved programs:
During unit testing, runerror operates in testing mode, and it behaves a little dif‐
ferently (Section F.5.1, page S182).
Function synerror is like runerror, except that before its format string, it takes
an argument of type Sourceloc, which tracks the source‐code location being read
at the time of the error. The location can be printed as part of the error message.
47b. hshared function prototypes 43bi+≡ (S295a) ◁ 47a 47c ▷
void synerror (Sourceloc src, const char *fmt, ...); type Exp A
type Sourceloc
Error handling, as opposed to error signaling, is implemented by calling setjmp S293h
on errorjmp. Function setjmp must be called before any error‐signaling func‐
tion. It is an unchecked run‐time error to call runerror or synerror except when
a setjmp involving errorjmp is active on the C call stack.
One common run‐time error is that an Impcore function is called with the
wrong number of arguments. That error is detected by function checkargc. Its pa‐
rameter e holds the call in which the error might occur.
47c. hshared function prototypes 43bi+≡ (S295a) ◁ 47b
void checkargc(Exp e, int expected, int actual);
1.6.2 Implementation of the evaluator
1 Evaluating expressions
Function eval implements the ⇓ relation from the operational semantics. Calling
eval(e, ξ, ϕ, ρ) finds a v , ξ ′ , and ρ′ such that he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i, assigns
An imperative core ρ := ρ′ and ξ := ξ ′ , and returns v . Because Greek letters aren’t customary in
C code, I use these English names:
48
ξ globals
ϕ functions
ρ formals
The assertion at the end of eval might seem superfluous, but it isn’t; it helps pro‐
tect me, and you, from mistakes, and it convinces the C compiler that every possi‐
ble case is covered.
Function eval proceeds by case analysis over the syntactic forms of Exp. Each
case is written in consultation with the operational semantics: for each syntactic
form, eval implements the rules that have the form on the left‐hand sides of their
conclusions.
The LıTERAL form appears in the conclusion of just one rule.
(LıTERAL)
hLıTERAL(v), ξ, ϕ, ρi ⇓ hv, ξ, ϕ, ρi
The implementation returns the literal value.
48c. hevaluate e‑>literal and return the result 48ci≡ (48b)
return e‑>literal;
if (isvalbound(e‑>set.name, formals))
bindval(e‑>set.name, v, formals);
else if (isvalbound(e‑>set.name, globals))
bindval(e‑>set.name, v, globals);
else
runerror("tried to set unbound variable %n in %e", e‑>set.name, e);
return v;
} bindval 45b
eval 45c
The ıF form appears in the conclusions of two rules.
type Exp A
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 6= 0 he2 , ξ ′ , ϕ, ρ′ i ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i type Explist S292d
fetchval 44g
hıF(e1 , e2 , e3 ), ξ, ϕ, ρi ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i type Funenv 44e
(IFTRUE) isvalbound 45a
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 = 0 he3 , ξ ′ , ϕ, ρ′ i ⇓ hv3 , ξ ′′ , ϕ, ρ′′ i runerror 47a
hıF(e1 , e2 , e3 ), ξ, ϕ, ρi ⇓ hv3 , ξ ′′ , ϕ, ρ′′ i type Valenv 44e
(IFFALſE) type Value 43c
type Valuelist
Both rules have the same first premise: he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i. To get v1 , ξ ′ , 43c
and ρ′ , the code calls eval(e‑>ifx.cond, globals, functions, formals) recur‐
sively. This call may mutate the globals and formals environments, but regardless
of whether v1 = 0, the mutation is safe, because the third premises of both rules
use the new environments ξ ′ and ρ′ . Comparing v1 with zero determines which
rule should be used: the implementation ends with a recursive call to evaluate ei‐
ther e2 (e‑>ifx.truex) or e3 (e‑>ifx.falsex).
49c. hevaluate e‑>ifx and return the result 49ci≡ (48b)
if (eval(e‑>ifx.cond, globals, functions, formals) != 0)
return eval(e‑>ifx.truex, globals, functions, formals);
else
return eval(e‑>ifx.falsex, globals, functions, formals);
The WHıLE form appears in the conclusions of two rules.
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 6= 0
he2 , ξ ′ , ϕ, ρ′ i ′′
⇓ hv2 , ξ , ϕ, ρ i′′ hWHıLE(e1 , e2 ), ξ ′′ , ϕ, ρ′′ i ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
1 hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
(WHıLEITERATE)
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 = 0
(WHıLEEND)
hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ h0, ξ , ϕ, ρ′ i
′
An imperative core In the first rule, the premise hWHıLE(e1 , e2 ), ξ ′′ , ϕ, ρ′′ i ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i could
be implemented as a recursive call to eval(e, . . .). But e is always a while loop, so
50 I have optimized the code by turning the recursion into iteration. This optimization
prevents a long WHıLE loop from overflowing the C stack.
50a. hevaluate e‑>whilex and return the result 50ai≡ (48b)
while (eval(e‑>whilex.cond, globals, functions, formals) != 0)
eval(e‑>whilex.exp, globals, functions, formals);
return 0;
(EMPTYBEGıN)
hBEGıN(), ξ, ϕ, ρi ⇓ h0, ξ, ϕ, ρi
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
..
.
hen , ξn−1 , ϕ, ρn−1 i ⇓ hvn , ξn , ϕ, ρn i
(BEGıN)
hBEGıN(e1 , e2 , . . . , en ), ξ0 , ϕ, ρ0 i ⇓ hvn , ξn , ϕ, ρn i
A nonempty BEGıN is implemented by iterating over its subexpressions, leaving the
last value in variable lastval. If lastval is initialized to zero, the same code also
implements the empty BEGıN.
50b. hevaluate e‑>begin and return the result 50bi≡ (48b)
{
Value lastval = 0;
for (Explist es = e‑>begin; es; es = es‑>tl)
lastval = eval(es‑>hd, globals, functions, formals);
return lastval;
}
Function application appears in the conclusion of the APPLYUſER rule, and also
in every rule that describes a primitive function. The rule to be implemented de‐
pends on the form of the function, which may be USERDEF or PRIMITIVE. Given a
function named f (e‑>apply.name), the interpreter discovers its form by looking
at ϕ(f ), which it stores in local variable f.
50c. hevaluate e‑>apply and return the result 50ci≡ (48b)
{
Func f;
hmake f the function denoted by e‑>apply.name, or call runerror 51ai
switch (f.alt) {
case USERDEF: happly f.userdef and return the result 51ci
case PRIMITIVE: happly f.primitive and return the result 52ai
default: assert(0);
}
}
If f is not defined as a function, the result is a run‐time error.
51a. hmake f the function denoted by e‑>apply.name, or call runerror 51ai≡ (50c)
if (!isfunbound(e‑>apply.name, functions))
runerror("call to undefined function %n in %e", e‑>apply.name, e);
f = fetchfun(e‑>apply.name, functions);
Each arithmetic primitive expects exactly two arguments, which the code puts
in C variables v and w. The characters of the primitive’s name go in s.
52c. happly arithmetic primitive to vs and return 52ci≡ (52a)
{
checkargc(e, 2, lengthVL(vs));
Value v = nthVL(vs, 0);
Value w = nthVL(vs, 1);
const char *s = nametostr(f.primitive);
hif operation s would overflow on v and w, call runerror 53ai
hreturn a function of v and w determined by s 52di
}
But the interpreter cannot ignore the possibility of overflow. The rules of Impcore
are different from the rules of C, and if the result of an arithmetic operation does
not fit in the range −231 to 231 , the operation causes a checked run‐time error.
The error is detected and signaled by function checkarith, which is defined in Ap‐
pendix F.
53a. hif operation s would overflow on v and w, call runerror 53ai≡ (52c)
checkarith(s[0], v, w, 32);
As noted on page 24, definitions are divided into two forms: The true defini‐ §1.6
tions can differ in each language; Impcore’s true definitions include val and The interpreter
define. The extended definitions are shared across languages; they include use
and check‑expect. True definitions have an operational semantics; extended def‐ 53
initions don’t. And true definitions are evaluated by code that is explained here;
extended definitions are evaluated by code in the Supplement.
The → relation on the true definitions is implemented by function evaldef.
Calling evaldef(d, ξ, ϕ, echo) finds a ξ ′ and ϕ′ such that hd, ξ, ϕi → hξ ′ , ϕ′ i, and
evaldef mutates the C representation of the environments so the global‐variable
environment becomes ξ ′ and the function environment becomes ϕ′ . If echo is
ECHOING, evaldef also prints the interpreter’s response to the user’s input. Printing
the response is evaldef’s job because only evaldef can tell whether to print a value
(for EXP and VAL) or a name (for DEFINE).
Just like eval, evaldef looks at the syntactic form of d and implements what‐
ever rules have that form in their conclusions.
53b. heval.c 48ai+≡ ◁ 51b
void evaldef(Def d, Valenv globals, Funenv functions, Echo echo) {
switch (d‑>alt) {
case VAL: bindval 45b
hevaluate d‑>val, mutating globals 53ci checkargc 47c
return; checkarith S187b
case EXP: type Def A
type Echo S293f
hevaluate d‑>exp and possibly print the result 54ai
eval 45c
return;
evallist 48a
case DEFINE: formals 48b
hevaluate d‑>define, mutating functions 54bi functions 48b
return; type Funenv 44e
} globals 48b
assert(0);
lengthVL A
mkValenv 44f
}
nametostr 43b
A VAL form updates ξ . nthVL A
print S176d
he, ξ, ϕ, {}i ⇓ hv, ξ ′ , ϕ, ρ′ i runerror 47a
(DEFıNEGLOBAL) strtoname 43b
hVAL(x, e), ξ, ϕi → hξ ′ {x 7→ v}, ϕi type Valenv 44e
type Value 43c
The premise shows that value v and environment ξ ′ are obtained by calling eval. type Valuelist
This call uses an empty environment as ρ. In the conclusion, the new environ‐ 43c
ment ξ ′ is retained, and the value of the expression, v , is bound to x in it. Value v
may also be printed.
53c. hevaluate d‑>val, mutating globals 53ci≡ (53b)
{
Value v = eval(d‑>val.exp, globals, functions, mkValenv(NULL, NULL));
bindval(d‑>val.name, v, globals);
if (echo == ECHOING)
print("%v\n", v);
}
An EXP form also updates ξ , just as if it were a definition of it.
A DEFıNE form updates ϕ. The implementation may print the name of the func‐
tion being defined.
x1 , . . . , xn all distinct
hDEFıNE(f, hx1 , . . . , xn i, e), ξ, ϕi → hξ, ϕ{f 7→ UſER(hx1 , . . . , xn i, e)}i
(DEFıNEFUNCTıON)
54b. hevaluate d‑>define, mutating functions 54bi≡ (53b)
bindfun(d‑>define.name, mkUserdef(d‑>define.userfun), functions);
if (echo == ECHOING)
print("%n\n", d‑>define.name);
The evaluator does not check to see that the x1 , . . . , xn are all distinct—the xi ’s
are checked when the definition is parsed, by function check_def_duplicates in
chunk S208e.
An environment is represented by a pair of lists; one holds names and the other
holds the corresponding values. The lists have the same length. (A search tree or
hash table would be enable faster search but would be more complicated.)
54c. henv.c 54ci≡ 54d ▷
struct Valenv {
Namelist xs;
Valuelist vs;
// invariant: lists have the same length
};
A derivation is also called a proof tree; the root contains the conclusion, and each
subtree is also a derivation. A derivation tree is written with its root at the bottom;
as in a single rule, the conclusion of a derivation appears on the bottom, below a
horizontal line. The leaf nodes appear at the top; each leaf node is an instance of
an inference rule that has no evaluation judgments among its premises, like the
LıTERAL rule or the FORMALVAR rule (page 32). A leaf node corresponds to a com‐
putation in which eval returns a result without making a recursive call.
Each node in a derivation tree is obtained by instantiating a rule and then de‐
riving that rule’s premises. Instantiation may substitute for none, some, or all of
a rule’s metavariables. Substitution that replaces all metavariables, leaving only
complete environments, syntax, and data, describes a single run of eval. For ex‐
ample, suppose a literal 83 is evaluated in a context where there are no global vari‐
ables, no functions, and no formal parameters:
eval(mkLiteral(83),
mkValenv(NULL, NULL), mkFunenv(NULL, NULL), mkValenv(NULL, NULL));
The resulting run can be described by an instance of the LıTERAL rule. The rule
appears in the semantics as follows:
. (LıTERAL)
hLıTERAL(v), ξ, ϕ, ρi ⇓ hv, ξ, ϕ, ρi
The instance that describes the run is obtained by substituting 83 for v , {} for ξ ,
{} for ϕ, and {} for ρ:
.
hLıTERAL(83), {}, {}, {}i ⇓ h83, {}, {}, {}i
Because the LıTERAL rule has no premises above the line, this instance is a com‐
plete, valid derivation all by itself.
The preceding example is awfully specific. In practice, a literal 83 evaluates
to 83 regardless of the presence of functions or variables—and the evaluation does
not change the values of any variables. To prove that, I create a different instance
of LıTERAL, in which I substitute only for v , leaving ξ , ϕ, and ρ as they are written
in the rule:
.
hLıTERAL(83), ξ, ϕ, ρi ⇓ h83, ξ, ϕ, ρi
This instance is also a complete, valid derivation, and it describes the evaluation of
a literal 83 in any possible environment.
A complete derivation tree ends in a single rule, but if that rule has evaluation
judgments above the line, like the APPLY rules, then each of those judgments—the
premises—must be derived as well. A complete derivation follows this schema:
Derivation of Derivation of
···
first premise last premise
. §1.7
Derivation =
Conclusion Operational
semantics
This schema assumes that every premise is justified by a derivation. In practice, revisited: Proofs
only an evaluation judgment can be justified by a derivation. Other premises, like
57
x ∈ dom ρ or ρ(x) = 3, are justified by appealing to what we know about x and ρ.
As an example of a derivation tree with more than one node (which is wide
enough to extend into the margin), I describe the evaluation of (+ (* x x) (* y y)),
which computes the sum of two squares. It’s evaluated in an environment where
ρ binds x to 3 and y to 4.
x ∈ dom ρ ρ(x) = 3 x ∈ dom ρ ρ(x) = 3
FORMALVAR FORMALVAR
hVAR(x), ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi hVAR(x), ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi
APPLYMUL
hAPPLY(*, VAR(x), VAR(x)), ξ, ϕ, ρi ⇓ h9, ξ, ϕ, ρi ···
APPLYADD
hAPPLY(+, APPLY(*, VAR(x), VAR(x)), APPLY(*, VAR(y), VAR(y))), ξ, ϕ, ρi ⇓ h25, ξ, ϕ, ρi
Each node is labeled with the name of the rule to which it corresponds. Because
derivation trees take so much space, I’ve elided the subtree that proves
Derivation trees can get big, and to fit them into small spaces, we often
take liberties with notation. For example, instead of writing abstract syntax like
APPLY(*, VAR(y), VAR(y)), we can write concrete syntax like (* y y). The resulting
notation is easier to digest, but it is less obvious that each node is an instance of a
semantic rule:
∈ dom ρ
x ρ(x) = 3 x ∈ dom ρ ρ(x) = 3
FORMALVAR FORMALVAR
hx, ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi hx, ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi
APPLYMUL
h(* x x), ξ, ϕ, ρi ⇓ h9, ξ, ϕ, ρi ···
APPLYADD .
h(+ (* x x) (* y y)), ξ, ϕ, ρi ⇓ h25, ξ, ϕ, ρi
Even if I use a smaller font and don’t label the nodes, the full derivation tree sticks
even further into the margin:
x ∈ dom ρ ρ(x) = 3 x ∈ dom ρ ρ(x) = 3 y ∈ dom ρ ρ(y) = 4 y ∈ dom ρ ρ(y) = 4
hx, ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi hx, ξ, ϕ, ρi ⇓ h3, ξ, ϕ, ρi hy, ξ, ϕ, ρi ⇓ h4, ξ, ϕ, ρi hy, ξ, ϕ, ρi ⇓ h4, ξ, ϕ, ρi
h(* x x), ξ, ϕ, ρi ⇓ h9, ξ, ϕ, ρi h(* y y), ξ, ϕ, ρi ⇓ h16, ξ, ϕ, ρi
.
h(+ (* x x) (* y y)), ξ, ϕ, ρi ⇓ h25, ξ, ϕ, ρi
• Rule R may include evaluation judgments above the line, as premises. After
substitution, each of these premises must be justified by a derivation of its
own.
An imperative core
• After substitution, every other premise in rule R must also be justified.
58 Premises that aren’t evaluation judgments are usually justified by set theory,
arithmetic, or appeal to assumptions.
As an example, if n is a formal parameter, evaluating (set n 0) sets n to zero
and returns zero. I want to derive the judgment
If we know something about the environments, we can use derivation trees to an‐
swer questions about the evaluation of expressions and definitions. One example
is the evaluation of the expression in Exercise 12 on page 77. This kind of appli‐
cation of the language semantics is called the theory of the language. But we can
answer much more interesting questions if we prove facts about derivations. For ex‐
ample, in Impcore, the expression (if x x 0) is always equivalent to x (Exercise 13
on page 77). Because a computation is a sequence of events in time, proving facts
about computations can be difficult. But if every terminating computation is de‐
scribed by a derivation, a derivation is just a data structure, and proving facts about
data structures is much easier. Reasoning about derivations is called metatheory.
If you already know that you want to study metatheory, you’ll soon need another
book. This book only suggests what metatheory can do, so you can figure out if you
want to study it more deeply.
n ∈ dom ρ n ∈ dom ρ
hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi
n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi
1 h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i
n ∈ dom ρ n ∈ dom ρ
hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi
n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi
An imperative core h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i
60 n ∈ dom ρ n ∈ dom ρ
hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi
n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi
h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i
n ∈ dom ρ n ∈ dom ρ
hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi
n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi
h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i
n ∈ dom ρ
hn, ρi ⇓ h7, ρi h1, ρi ⇓ h1, ρi
n ∈ dom ρ h(+ n 1), ρi ⇓ h8, ρi
h(set n (+ n 1)), ρi ⇓ h8, ρ{n 7→ 8}i
Metatheory can determine the validity of a claim like this: in any Impcore pro‐
gram, the expression (+ x 0) can be replaced by just x, and this replacement doesn’t
change any output of the program. This claim could be important to a compiler
writer, who might use it to create an “optimization” that improves performance.
If you’re going to create an optimization, you must be certain that it doesn’t change
the meaning of any program. Certainty can be supplied by a metatheoretic proof.
What can we prove about a program that evaluates (+ x 0)? If a derivation ex‐
ists, it will contain a judgment of the form
That judgment will be the root of a subderivation. That subderivation must apply
a rule that permits the application of the + primitive on the left‐hand side of its
conclusion. So just like the interpreter code in chunk 48b, a proof has to consider
inference rules whose conclusions can contain APPLY(+, . . .).
Two such rules appear in Section 1.5.6: APPLYUſER and APPLYADD. And the
claim is false! If ϕ(+) refers to a user‐defined function, the APPLYUſER rule kicks in,
and it is not safe to replace (+ x 0) with x. Here’s a demonstration:
60. hterrifying transcript 60i≡
‑> (define + (x y) y) ; no sane person would do this
‑> (define addzero (x) (+ x 0))
‑> (addzero 99)
0
‑> (define addzero2 (x) x)
‑> (addzero2 99)
99
If a compiler writer wants to be able to replace an occurrence of (+ x 0) with x,
they will first have to prove that the environment ϕ in which (+ x 0) is evaluated
never binds + to a user‐defined function. (Compilers typically include lots of infras‐
tructure for proving facts about environments, but such infrastructure is beyond
the scope of this book.)
Proving facts about derivations is metatheory. Metatheory enables you to prove
§1.7
properties like these:
Operational
• Expression (if x x 0) is equivalent to x (Exercise 13). semantics
revisited: Proofs
• If evaluation of a while loop terminates, its value is zero (Exercise 23).
61
• Evaluating an expression can’t create a new variable (Exercise 24).
Results like these are useful, but one requires a long, detailed proof.
• When the last rule used in D is FORMALAſſıGN, the derivation must have the
following form:
D1
x ∈ dom ρ he1 , ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i
D= FORMALAſſıGN
hſET(x, e1 ), ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ {x 7→ v}i
The form of e is ſET(x, e1 ). Our obligation is to prove that the induction hy‐
pothesis holds for the judgment below the line. We must therefore prove
that dom ξ = dom ξ ′ . But because derivation D1 is smaller than deriva‐
tion D , we are permitted to assume the induction hypothesis, which tells us
that dom ξ = dom ξ ′ . Our obligation is met.
This example, like all the cases in my metatheoretic proofs, uses the following tem‐
plate:
1. When the last rule used in D is RULENAME, and RULENAME has conclu‐
sion C and premises P1 to Pn , the derivation must have the following form:
P1 ··· Pn
D= RULENAME
C
Commentary: The conclusion C is the evaluation judgment of which D is a proof.
If any particular Pi is also an evaluation judgment, I write its derivation above it,
as in
1 D1
P1 ···
Di
Pi ···
Dn
Pn
D= RULENAME
C
A premise like “x ∈
/ dom ρ” is not an evaluation judgment and is not supported
An imperative core
by a subderivation.
62
2. The form of e is syntactic form, and whatever additional analysis goes with that
syntactic form and with rule RULENAME.
3. Our obligation is to prove that the induction hypothesis holds for the judg‐
ment below the line. We must therefore prove whatever it is.
5. From the truth of premises P1 to Pn , plus the information from the induction
hypothesis, we show that the induction hypothesis holds for the judgment below
the line.
This template has served me well, but part of it may surprise you: it doesn’t dis‐
tinguish between “base cases” and “inductive cases.” The distinction exists—a base
case is one that has no evaluation judgments above the line—but in a programming‐
language proof, the distinction is not terribly useful. For example, base cases might
or might not be easy, and they might or might not fail.
The template is instantiated for every case in a proof. As a demonstration, I in‐
stantiate the template to try to prove the metatheoretic conjecture,
(This conjecture isn’t true, but that’s a good thing—we learn the most from the
things we try to prove that aren’t so. To maximize your own learning, you might
pause and think about which case or cases of the proof are going to fail.)
To begin, I state my conjecture formally. And that means I must formalize the
idea of “every variable in e.” I use the function fv(e), short for “free variables of e,”
which is defined in Figure 1.8 on the facing page. Figure 1.8 uses a simplified dialect
of Impcore, which makes the metatheoretic proof a little easier:
(You can work out how to eliminate begin in Exercise 14 on page 79.) Informally
speaking, fv(e) is the set of variables mentioned in e. A variable can be mentioned
directly only in a VAR expression or in a ſET expression. In other forms of expres‐
sion, the free variables are the free variables of the subexpressions.
fv(LıTERAL(v)) = ∅
fv(VAR(x)) = {x}
fv(ſET(x, e)) = {x} ∪ fv(e)
fv(ıF(e1 , e2 , e3 )) = fv(e1 ) ∪ fv(e2 ) ∪ fv(e3 )
§1.7
fv(WHıLE(e1 , e2 )) = fv(e1 ) ∪ fv(e2 ) Operational
fv(APPLY(f, e1 , e2 )) = fv(e1 ) ∪ fv(e2 ) semantics
revisited: Proofs
• When the last rule used in D is LıTERAL, the derivation must have the fol‐
lowing form:
D= LıTERAL
hLıTERAL(v), ξ, ϕ, ρi ⇓ hv, ξ, ϕ, ρi
• When the last rule used in D is FORMALVAR, the derivation must have the
following form:
x ∈ dom ρ
D= FORMALVAR
hVAR(x), ξ, ϕ, ρi ⇓ hρ(x), ξ, ϕ, ρi
The form of e is VAR(x). Our obligation is to prove that the induction hy‐
pothesis holds for the judgment below the line. We must therefore prove
fv(VAR(x)) ⊆ dom ξ ∪dom ρ. According to the definition of fv in Figure 1.8,
fv(VAR(x)) = {x}. And from the first and only premise of the derivation,
we know that x ∈ dom ρ. Therefore
• When the last rule used in D is GLOBALVAR, the derivation must have the
following form:
The form of e is VAR(x). Our obligation is to prove that the induction hy‐
pothesis holds for the judgment below the line. We must therefore prove
fv(VAR(x)) ⊆ dom ξ ∪dom ρ. According to the definition of fv in Figure 1.8,
fv(VAR(x)) = {x}. And from the second premise of the derivation, we know
that x ∈ dom ξ . Therefore
• When the last rule used in D is FORMALAſſıGN, the derivation must have the
following form:
An imperative core
D1
64 x ∈ dom ρ he1 , ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i
D= FORMALAſſıGN
hſET(x, e1 ), ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ {x 7→ v}i
From the first premise of the derivation, we know that x ∈ dom ρ, and as be‐
fore, that implies (a). From the second premise of the derivation, we can ap‐
ply the induction hypothesis to D1 , which gives us (b). Our obligation is met.
• When the last rule used in D is IFTRUE, the derivation must have the follow‐
ing form:
D1 D2
′ ′
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ , ϕ, ρ i v1 6= 0 he2 , ξ , ϕ, ρ i ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i
′ ′
D= IFTRUE
hıF(e1 , e2 , e3 ), ξ, ϕ, ρi ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i,
• I could continue with the other cases. The proof fails for IFFALſE as well as
IFTRUE, but it succeeds for EMPTYBEGıN, BEGıN, and APPLYUſER. Analysis
of rules WHıLEITERATE and WHıLEEND is left for Exercise 26.
The conjecture isn’t actually a theorem, and in the process of working out a proof, §1.7
I found a counterexample. To guarantee that every variable in an expression is Operational
defined, we would need something stronger than a successful evaluation. Like a semantics
type checker, for example (Chapter 6). revisited: Proofs
65
1.7.4 Why bother with semantics, proofs, theory, and metatheory?
What’s up with all the Greek letters and horizontal lines? What’s the point? Isn’t it
easier just to look at the code? No, because an operational semantics leaves out all
sorts of “implementation details” that would otherwise impede our understanding
of how a language works. For example, to a compiler writer, the representation
of an environment is super important—where values are stored has a huge impact
on the performance of programs. But if we just want to understand how programs
behave, we don’t care. And once you get used to the Greek letters and horizontal
lines, you’ll find them easier to read than code—much easier. The point of opera‐
tional semantics is to combine precision and understanding. That’s why when you
find a new idea in a professional paper, the idea is usually nailed down using op‐
erational semantics. When you can read operational semantics, you’ll be able to
learn about new ideas for yourself, direct from the sources, instead of having to
find somebody to explain them to you.
What about proof theory and metatheory? Theory involves making derivations—
typically one derivation at a time. It can guide an implementor, because it tells
them just what each construct is supposed to do. In principle, theory could also
guide a programmer, who also needs to know what programs are supposed to
do. But in practice, operational semantics works at too low a level. A program‐
mer can more effectively use something like the algebraic laws in the next chapter.
The programmer—or perhaps a specialist—uses operational semantics to show that
the laws are sound, and after that, programming proceeds by appealing to the laws,
not to the operational semantics directly.
So theory is good for building implementations and for establishing algebraic
laws, both of which are useful for programmers. What is metatheory good for?
Metatheory involves reasoning about derivations. In particular, metatheory can re‐
veal universal truths about derivations, which correspond to facts about all pro‐
grams in a given language. Such truths might interest implementors, program‐
mers, or even policy makers. For example,
• If you’re implementing C or Impcore, you can keep the local variables and
formal parameters of all functions on a stack (Exercise 29). This stack is
called the call stack.
• In Impcore, no function can change the value of a formal parameter (or local
variable) belonging to any other function.
• In C, a function can change the value of a formal parameter (or local vari‐
able) belonging to another function, but only if at some point the & operator
was applied to the parameter or variable in question—or if somebody has
exploited “undefined behavior” with pointers.
• If an ordinary device driver fails, it can take down a whole operating‐system
kernel, resulting in a “blue screen of death.” But if a device driver is writ‐
ten in the special‐purpose language Sing#, the worst it can do is take away
its device—metatheory guarantees that the operating‐system kernel and the
1 other drivers are unaffected.
Serious metatheory is well worth learning; this book provides just a taste. Doing a
few of the exercises can show you the difference between theory and metatheory,
give you an idea of how a metatheoretic proof is structured, and give you an idea of
An imperative core what metatheory can do.
66
1.8 EXTENDıNG IMPCORE
Impcore is a “starter kit” for learning about abstract syntax, operational semantics,
and interpreters. It’s not a useful programming language—useful languages offer
more values than just machine integers. (New values are coming in Chapter 2.) But
even with only integer values, Impcore can still be extended in two useful ways:
with local variables and with looping constructs.
Any language that even pretends to be useful offers some species of local vari‐
ables. Impcore can offer them too (Exercise 30 on page 86). Local variables can be
used to define functions like the one shown in Figure 1.9 on the next page, which
adds up the odd numbers from 1 to n.
Adding local variables requires you to change the abstract syntax of Userfun so
that it includes not only a body and a list of formal parameters, but also a list of local
variables. And to account for the semantics of local variables, you’ll need to change
the evaluator. But other kinds of extensions, including new looping constructs, can
be implemented without touching the abstract syntax or the evaluator. You add only
concrete syntax, which is implemented in terms of the abstract syntax you have
already. This kind of new concrete syntax is called syntactic sugar.
As examples of syntactic sugar, I suggest several new ways to write loops. Let’s
begin with an ordinary while loop, like this one:
(while (<= i n)
(begin
(set sum (+ sum i))
(set i (+ i 2))))
The begin might seem like a lot of syntactic overhead. I imagine a new syntactic
form, which I’ll call while*, in which the condition is still a single expression, but
the body is a sequence of expressions. Now the begin is no longer necessary:
(while* (<= i n)
(set sum (+ sum i))
(set i (+ i 2)))
Finally, C’s complicated four‐part for loop can also be defined as syntactic sugar:
△
(for pre test post body ) = (begin pre (while test (begin body post )))
67. hanswer transcript 67i≡
‑> (define add‑odds‑to (n)
[locals i sum]
(begin
(set i 1)
(set sum 0)
(while (<= i n)
(begin
(set sum (+ sum i))
§1.9
(set i (+ i 2))))
Summary
sum))
‑> (add‑odds‑to 3) 67
4
‑> (add‑odds‑to 5)
9
‑> (add‑odds‑to 7)
16
All these alternatives can be implemented just by modifying Impcore’s parser (Ex‐
ercise 34, page 87). An example can be found in the Supplement (Section G.7,
page S209).
1.9 SUMMARY
BAſıſ A basis comprises all the information available about a particular set of
names. In Impcore, a basis is a pair of environments hϕ, ξi. A basis pro‐
vides the context used to evaluate a definition, and evaluating a definition
typically extends or alters the current basis. The basis available at startup is
called the ıNıTıAL BAſıſ.
What is syntactic sugar and who benefits?
1 any operational semantics. Some examples appear in the text: C’s do. . .while
and C’s for loop can be defined as syntactic sugar for various combinations of
while and begin (page 66). Such syntactic sugar can benefit programmers, im‐
plementors, designers, theorists, and other tool builders.
Programmers benefit most from syntactic sugar when they don’t know it’s there.
An imperative core
Syntactic sugar is defined by translation, which is not easy to think about—if ev‐
68 ery time you want to use a do‑while you first have to mentally translate it into
something else, that’s not an aid; it’s a stumbling block.
Implementors can benefit from syntactic sugar. If you implement a desugaring
transformation on your syntax, then without any other change to your compiler
or interpreter, you have a new language feature (Exercise 34). But as soon as
your implementation gets serious—say you want to check types, as in Chapter 6,
or you want to provide source‐level‐debugging—the syntactic sugar is not so use‐
ful, because you need to report errors or state in terms of the syntax the user
wrote originally, not the desugared form.
Language designers and theorists benefit the most from syntactic sugar. For
example, let’s say you’ve completed Exercise 29: you’ve proven that Impcore
can be evaluated on a stack. Now you want to add do‑while, for, while*, or
some other shiny new syntactic form. If the new form is just sugar—that is,
if it is defined by translation into the original syntax, which you used in your
proof—then you know the new, extended Impcore can still be evaluated on a
stack. You don’t have to consider any new cases in your proof, and you don’t
have to revisit any cases that you’ve already proven. This scenario describes a
very effective use of syntactic sugar: a careful language designer benefits from
small language, which is easy to prove things about, but the users benefit from
a larger language, which is more attractive and makes it easier to say things
idiomatically. Using syntactic sugar, a designer can have both.
I have assumed that new syntactic sugar can be created only by a language de‐
signer or implementor. This assumption holds for most languages, including
C and Impcore. the vast majority of other languages. But using Lisp, Scheme,
and related languages, new syntactic sugar can be created by ordinary program‐
mers. This capability gives programmers many of the same powers as language
designers (Section 2.14.4, page 171).
GRAMMAR A set of formal rules that enumerates all the ſYNTACTıC FORMſ in each
ſYNTACTıC CATEGORY. A grammar produces the set of all programs that are
grammatically well formed. A grammar can be designed to support a simple
decision procedure that tells if a particular utterance was produced by the
grammar, and if so, how. Such a decision procedure is embodied in a PARſER.
INıTıAL BAſıſ The BAſıſ used when first evaluating a user’s code. The initial basis
contains all the PRıMıTıVE FUNCTıONſ and PREDEFıNED FUNCTıONſ.
JUDGMENT FORM A template for creating ȷUDGMENTſ. For example, the form of
the evaluation judgment for Impcore is he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i. A judg‐
ment form is transformed into to a ȷUDGMENT by substituting ABſTRACT ſYN‐
TAX, values, ENVıRONMENTſ, or other entities for its METAVARıABLEſ.
METATHEORETıC PROOF A proof of a fact that is true of all valid derivations. Nor‐
mally proceeds by ſTRUCTURAL ıNDUCTıON on derivations.
1 d for a definition
x for a program variable
v for a value
ρ for an ENVıRONMENT
σ for a store (Chapter 2)
An imperative core
The name of a metavariable tells a reader what kind of thing it stands for, but
70 unfortunately, no two authors agree on names. Many authors, like Harper
(2012), use a mix of Greek and Roman letters, but both Pierce (2002) and
Cardelli (1989) use only Roman letters.
THEORY Theorems about programs. More broadly, mathematical tools that spec‐
ify meaning and behavior of programs. Theory can be used to prove facts
about individual programs and to specify an EVALUATOR or type checker.
In this book, the theory of a language is its operational semantics plus, in
Chapters 6 to 9, its type system. A language’s theory may be used to create a
DEFıNıTıONAL ıNTERPRETER. Compare it with METATHEORY.
1.10 EXERCıſEſ
If you read this book without doing any of the exercises, you’ll miss most of what
it has to offer. But don’t try to do all the exercises; you’ll die of overwork. Choose
your exercises well and you’ll have a great experience.
To help you find exercises, I’ve organized them by the skill they demand.
In each chapter, you’ll find a table of exercises that lists each group of exercises
along with the skills they develop and the reading that is most necessary. Skills
often include programming in a bridge language, working with a semantics, and
modifying an interpreter—but there are others as well. In this chapter, the skills
are listed in Table 1.10.
Each chapter’s exercises are preceded by highlights. The highlights list exercises
that are my personal favorites, or that I think are the best, or that I often assign
to my students. And each chapter’s exercises begin with questions that support
retrieval practice. These very short questions will help you keep the essential ideas
at the surface of your mind, so you can do the exercises fluently. And if you are a
student in a university course, they may help you study for exams.
The highlights of this chapter’s exercises are as follows:
• You should write at least one derivation (Exercise 12 on page 77). To start rea‐
soning about derivations, follow up with Exercise 13 or 23 on pages 77 or 83.
• You can do some metatheory. The very best of the metatheoretic exercises
are Exercises 25 and 29 (pages 83 and 85). You may have to work up to
them, but if you tackle either, or better yet both, you will understand which §1.10
rules of the operational semantics are boring and straightforward and which Exercises
rules have interesting and important consequences. Exercises 24 and 27 on
73
pages 83 and 84 are significantly easier but also worthwhile.
• To get some practice with the interpreter, add local variables to Impcore (Ex‐
ercise 30). It will help you think about the connection between semantics and
implementation.
Some of the exercises in these first three sections are adapted from Kamin (1990,
Chapter 1), with permission.
1. Understanding scope, from C to Impcore. The scope rules of Impcore are iden‐
tical to that of C. Consider this C program:
74. hmystery.c 74i≡
int x;
1 void R(int y) {
x = y;
}
void main(void) {
x = 2;
Q(4);
printf("%d\n", x);
}
(a) For each occurrence of x in the C program, identify whether the occur‐
rence refers to a global variable or a formal parameter.
(b) Say what the C program prints.
(c) Write, in Impcore, a sequence of four definitions that correspond to the
C program. Instead of printf, call println.
3. Exponential and logarithm. Define functions exp and log. When base b and
exponent n are nonnegative, (exp b n) = bn , and when b > 1 and m > 0,
(log b m) is the smallest integer n such that bn+1 > m. On inputs that don’t
satisfy the preconditions, your implementation may do anything you like—
even fail to terminate.
4. The nth Fibonacci number. Define a function fib such that (fib n) is the nth
Fibonacci number. The Fibonacci numbers are a sequence of numbers de‐
fined by these laws:
(fib 0) =0
(fib 1) = 1
(fib n) = (fib (‑ n 1)) + (fib (‑ n 2)) when n > 1
These identities guarantee that if the answer is small enough to fit in a ma‐
chine word, then the results of all of the intermediate computations are also
small enough to fit in a machine word.
(b) Write a function all‑fours?, which when given any number, returns
1 if its decimal representation is all fours and 0 otherwise. You could
define a function like this:
75b. hunsatisfying answer 75bi≡
(define all‑fours? (n)
(if (> n 0) (given‑positive‑all‑fours? n) 0))
11. Translating English into formal judgments. Take each of the following informal
statements and restate it using the formalism of operational semantics.
To help you with the operational‐semantics exercises, the rules of Impcore’s oper‐
ational semantics are summarized in Figures 1.11 and 1.12.
12. Proof of the result of evaluation. Use the operational semantics to prove that if
you evaluate (begin (set x 3) x) in an environment where ρ(x) = 99, then
the result of the evaluation is 3. In your proof, use a formal derivation tree
like the example on page 57.
13. Proof of equivalence of two expressions. Show that expression (if x x 0) is ob
servationally equivalent to just x. That is, show that the two expressions can
be interchanged in any program, and if we run both variants, we won’t be
able to observe any difference in behavior.
(LıTERAL)
hLıTERAL(v), ξ, ϕ, ρi ⇓ hv, ξ, ϕ, ρi
x ∈ dom ρ
1 hVAR(x), ξ, ϕ, ρi ⇓ hρ(x), ξ, ϕ, ρi
(FORMALVAR)
x∈
/ dom ρ x ∈ dom ξ he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i
(GLOBALAſſıGN)
hſET(x, e), ξ, ϕ, ρi ⇓ hv, ξ ′ {x 7→ v}, ϕ, ρ′ i
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 6= 0
he2 , ξ ′ , ϕ, ρ′ i ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i hWHıLE(e1 , e2 ), ξ ′′ , ϕ, ρ′′ i ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ hv3 , ξ ′′′ , ϕ, ρ′′′ i
(WHıLEITERATE)
he1 , ξ, ϕ, ρi ⇓ hv1 , ξ ′ , ϕ, ρ′ i v1 = 0
(WHıLEEND)
hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ h0, ξ , ϕ, ρ′ i
′
(EMPTYBEGıN)
hBEGıN(), ξ, ϕ, ρi ⇓ h0, ξ, ϕ, ρi
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
he2 , ξ1 , ϕ, ρ1 i ⇓ hv2 , ξ2 , ϕ, ρ2 i
..
.
hen , ξn−1 , ϕ, ρn−1 i ⇓ hvn , ξn , ϕ, ρn i
(BEGıN)
hBEGıN(e1 , e2 , . . . , en ), ξ0 , ϕ, ρ0 i ⇓ hvn , ξn , ϕ, ρn i
ϕ(f ) = UſER(hx1 , . . . , xn i, e)
x1 , . . . , xn all distinct
he1 , ξ0 , ϕ, ρ0 i ⇓ hv1 , ξ1 , ϕ, ρ1 i
..
.
hen , ξn−1 , ϕ, ρn−1 i ⇓ hvn , ξn , ϕ, ρn i
he, ξn , ϕ, {x1 7→ v1 , . . . , xn 7→ vn }i ⇓ hv, ξ ′ , ϕ, ρ′ i
(APPLYUſER)
hAPPLY(f, e1 , . . . , en ), ξ0 , ϕ, ρ0 i ⇓ hv, ξ ′ , ϕ, ρn i
x1 , . . . , xn all distinct
hDEFıNE(f, hx1 , . . . , xn i, e), ξ, ϕi → hξ, ϕ{f 7→ UſER(hx1 , . . . , xn i, e)}i
(DEFıNEFUNCTıON)
(a) Use the operational semantics to show that if there exist environments
ξ , ϕ, and ρ (and ξ ′ , ρ′ , ξ ′′ , and ρ′′ ) such that
and
hVAR(x), ξ, ϕ, ρi ⇓ hv2 , ξ ′′ , ϕ, ρ′′ i
then v1 = v2 .
(b) Now use the operational semantics to show that there exist environ‐
ments ξ , ϕ, ρ, ξ ′ , and ρ′ and a value v1 such that
14. Proof that begin can be eliminated. Impcore can be simplified by eliminating
the begin expression—every begin can be replaced with a combination of
function calls. For this problem, assume that ϕ binds the function second
according to the following definition:
(define second (x y) y)
I claim that if e1 and e2 are arbitrary expressions, you can always write
(second e1 e2 ) instead of (begin e1 e2 ).
(a) Using evaluation judgments, take the claim “you can always write
(second e1 e2 ) instead of (begin e1 e2 )” and restate the claim in pre‐
cise, formal language.
Hint: The claim is related to the claims in Exercise 13.
(b) Using operational semantics, prove the claim.
(c) Define a translation for (begin e1 · · · en ) such that the translated code
behaves exactly the same as the original code, but in the result of the
translation, every remaining begin has exactly two subexpressions.
For example, you might translate
(begin e1 e2 e3)
into
1 (begin e1 (begin e2 e3))
If you apply the translation recursively, then replace every begin with a call
to second, you can eliminate begin entirely.
An imperative core
1.10.8 Operational semantics: Writing new rules
80
15. Operational semantics of a for loop. Give operational semantics for a C‐like
FOR(e1 , e2 , e3 , e4 ). Like a while expression, a for expression is evaluated
for its side effects, so the value it returns is unimportant. Choose whatever
result value you like.
(a) Change the rules of Impcore as needed, and add as many new rules as
needed, to give Impcore Awk‐like semantics for unbound variables.
(b) Change the rules of Impcore as needed, and add as many new rules as
needed, to give Impcore Icon‐like semantics for unbound variables.6
(c) Which of the two changes do you prefer, and why?
(d) Create a program that can distinguish standard Impcore semantics
from the Awk‐like and Icon‐like extensions described above. In partic‐
ular, create a source file awk‑icon.imp containing a sequence of defi‐
nitions with the following properties:
• Every definition in the sequence is syntactically valid Impcore.
• If you present the sequence of definitions to a standard Impcore
interpreter, the result is a checked run‐time error.
• If you present the sequence of definitions to an Impcore inter‐
preter that has been extended with the Awk‐like semantics, the last
thing the interpreter does is print 1.
• If you present the sequence of definitions to an Impcore inter‐
preter that has been extended with the Icon‐like semantics, the
last thing the interpreter does is print 0.
17. Formal semantics of unit tests. The semantics of extended definitions hasn’t
been formalized. But some of the pieces can be formalized easily enough:
(a) Design a judgment form to express the idea that “a check‑expect test
succeeds.” Your judgment form should include environments ϕ and ξ .
Write a proof rule for the new judgment form.
(b) Design a judgment form to express the idea that “a check‑expect test
fails.” Write a proof rule for it.
6
Impcore has top‐level expressions, and Icon does not. For purposes of this problem, assume that
every top‐level expression is evaluated in its own, anonymous procedure.
(c) Design a judgment form to express the idea that “a check‑error test
fails.” Write a proof rule for it.
18. Meanings of numerals. A numeral is what we use to write numbers. Like an §1.10
Impcore expression or definition, a numeral is syntax. Decimal numerals Exercises
can be defined using a grammar: a decimal numeral N10 is composed of 81
decimal digits d:
d ::= 0 1 2 3 4 5 6 7 8 9
(The expression 10 · D[[N10 ]] means “10 times D[[N10 ]]”; as described in Ap‐
pendix B, this book uses the × symbol only for type theory.) In this exercise,
you write a similar specification for binary numerals.
19. Proof systems for program analysis: having set. Impcore is an imperative core
because it uses side effects. Of these side effects, the most important is mu
tation, also known as assignment.7 In Impcore, assignment is implemented
by set. In this exercise, you use proof theory to reason about a very simple
property: whether an expression has set in it. Looking forward, in Exer‐
cise 25, you can see what you can prove if you know an expression doesn’t
have a set.
To see if an expression has set, you can just look at it. But that’s a plan for a
person, not an algorithm for a computer or a set of rules for a proof. The idea
7
The other side effects are printing and use.
of “expression e has a set in it” can be made precise by introducing a judg‐
ment form e has ſET . The judgment is defined by this proof system:
SET
ei has ſET
BEGıN , i ∈ {1, . . . , n}
BEGıN(e1 , . . . , en ) has ſET
ei has ſET
APPLY , i ∈ {1, . . . , n}
APPLY(f, e1 , . . . , en ) has ſET
• There are no rules for variables or for literal values. And no wonder:
variables and literal values are expressions that don’t have set.
• There’s no premise on the rule for ſET. A set expression definitely has
set, no matter what’s true about its subexpressions.
• Any other expression has set if and only if one of its proper subexpres‐
sions has set. Expressing that idea requires a rule for each subexpres‐
sion. For ıF and WHıLE, the necessary rules can be written explicitly,
but BEGıN and APPLY require rule schemas. The notation i ∈ {1, . . . , n}
means that a rule is repeated n times: once for each value of i.
has set.
(b) Show that having set isn’t the same as evaluating set. Give an exam‐
ple of an expression e such that e has ſET, but you can guarantee that
evaluating e never evaluates a set.
20. Proof systems for program analysis: lacking set. When an expression has set,
the proof system in the previous problem tells us. But if an expression doesn’t
have set, that proof system tell us nothing! To know that expression doesn’t
have set requires another proof system. Develop a proof system for yet
another judgment form: e hasn’t ſET . Your proof system should derive
“e hasn’t ſET” exactly when expression e doesn’t have a set in it.
Your proof system’s structure should be related to the structure of the proof
system for “e has ſET.” The relationship is what a mathematician would call
dual:
• Where “e has ſET” lacks proof rules, such as for literals and variables,
“e hasn’t ſET” will have trivial proof rules with no premises.
• Where “e has ſET” has a trivial proof rule with no premises, such as for
set, “e hasn’t ſET” will lack proof rules. (There’s no way you can prove
that a set expression doesn’t have set.)
• Where e has subexpressions, for “e has ſET” it is sufficient to prove that
any of e’s subexpressions has a set. But for “e hasn’t ſET” it is necessary
to prove that all of e’s subexpressions have not got set. (This duality is §1.10
an instance of DeMorgan’s Law.) Exercises
Your proof system will be correct if every expression either has ſET or it 83
doesn’t (Exercise 22).
21. Proof system for checked runtime errors. To show when a check‑error test suc‐
ceeds, we need to be able to show when evaluation of an expression termi‐
nates with an error. Such a conclusion requires a pretty big proof system:
not quite as big as the complete operational semantics of Impcore, but bigger
than the proof systems for e has ſET and e hasn’t ſET in Exercises 19 and 20.
(a) Design a judgment form to express the idea that evaluation of an ex‐
pression terminates with an error. Your form will need all the same
environments as the form for evaluating an expression that produces a
value.
(b) Write a proof system for this judgment form.
(c) Design a judgment form to express the idea that “a check‑error test
succeeds.” Using your proof system from part (b), write a proof rule for
your new judgment form.
(d) If a run‐time error occurs during the evaluation of a check‑expect test,
that test is deemed to fail. To cover this possibility, write additional
proof rules for the judgment that “a check‑expect test fails.”
22. An expression either has ſET or it doesn’t. Show that the two judgments in Ex‐
ercises 19 and 20 are mutually exclusive and cover all cases. That is, for any
expression e, there is a valid derivation of exactly one of the two judgments
e has ſET and e hasn’t ſET. Try proof by induction on the syntactic structure
of e.
23. A WHıLE expression evaluates to zero. Prove that the value of a WHıLE expres‐
sion is always zero. That is, given any ξ , ϕ, ρ, e1 , and e2 , if there exist a ξ ′ , ρ′ ,
and v such that there is a derivation of hWHıLE(e1 , e2 ), ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i,
then v = 0. Use structural induction on the derivation.
24. Expression evaluation doesn’t add or remove global variables. Prove that the ex‐
ecution of an Impcore expression does not change the set of variables bound
in the global environment. That is, prove that if he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i,
then dom ξ = dom ξ ′ .
25. Program analysis and expression evaluation: does lacking ſET guarantee un
changed variables? Is it true or false that evaluating an expression without a
ſET node does not change any environment? Use metatheory to justify your
answer. To be sure you understand what it means to have a ſET node, see
Exercise 19.
26. Does evaluation guarantee defined variables? Section 1.7.3 on page 61 shows
how to attempt a metatheoretic proof, and it examines the conjecture “if ex‐
pression e evaluates, all its variables are defined.” Section 1.7.3 addresses
every rule except for WHıLEITERATE and WHıLEEND.
1 (a) For derivations ending in WHıLEITERATE, either prove the conjecture
or show an expression whose evaluation is a counterexample.
(b) For derivations ending in WHıLEEND, either prove the conjecture or
show an expression whose evaluation is a counterexample.
An imperative core
(c) Explain in informal English what is going on with while loops—when,
84 whether, and how do we know if a while loop’s variables are defined?
27. Impcore is deterministic. Prove that Impcore is deterministic. That is, prove
that for any e and any environments ξ , ϕ, and ρ, there is at most one v such
that he, ξ, ϕ, ρi ⇓ hv, ξ ′ , ϕ, ρ′ i.
You’ll reason about two potentially different derivations, each describing
its own evaluation of e. Think carefully about your induction hypothesis.
For example, to prove that expression (begin e0 x) evaluates to at most
one v , what do you need to know about the evaluation of e0 ?
ξ1 = ξ0 ρ1 = ρ0
x∈
/ dom ρ0 x ∈ dom ξ0 h3, ξ0 , ϕ, ρ0 i ⇓ h3, ξ1 , ϕ, ρ1 i ξ2 = ξ1 {x 7→ 3} ρ2 = ρ1
.
h(set x 3), ξ0 , ϕ, ρ0 i ⇓ h3, ξ2 , ϕ, ρ2 i
D
y ∈ dom ρ0 h(set x 3), ξ0 , ϕ, ρ0 i ⇓ h3, ξ2 , ϕ, ρ2 i ξ3 = ξ2 ρ3 = ρ2 {y 7→ 3}
.
h(set y (set x 3)), ξ0 , ϕ, ρ0 i ⇓ h3, ξ3 , ϕ, ρ3 i
In these derivations, each part of each state is given a name. For example,
in the first derivation, the environment ξ1 {x 7→ 3} is named ξ2 . The sub‐
scripts on the metavariables may help you see that, for example, ξ0 and ρ0
are used multiple times, but that after judgment h3, ξ0 , ϕ, ρ0 i ⇓ h3, ξ1 , ϕ, ρ1 i
is proved, only ξ1 and ρ1 are used thenceforth—environments ξ0 and ρ0 are
never used again. The transition from ξ1 and ρ1 to ξ2 and ρ2 is similar, and
so on. This observation suggests an optimization that is used in this chapter:
where the derivation says something like ξ2 = ξ1 {x 7→ 3}, the implementa‐
tion needn’t build a fresh environment ξ2 . It can instead simply update a data
structure: first the data structure holds ξ1 , then after the update, it holds ξ2 .
Show that this optimization does not affect the semantics:
(a) Prove that in any valid derivation, evaluation judgments can be totally
ordered by their use of the global‐variable environment. That is, the
premises required to prove any judgment can be ordered in such a way
that after the proof of every evaluation judgment, each of which takes
the form hei , ξi , ϕ, ρi i ⇓ hvi , ξi+1 , ϕ, ρi+1 i, global‐variable environ‐
ment ξi is never used again and can be discarded.
This metatheorem justifies using bindval(e‑>set.name, v, globals)
to overwrite the globals environment when set is evaluated.
(b) Show that uses of the formal‐parameter environment cannot be totally
ordered: Exhibit a valid derivation containing a judgment of the form
hei , ξi , ϕ, ρi i ⇓ hvi , ξi+1 , ϕ, ρi+1 i, such that the next evaluation judg‐ §1.10
ment has an initial state of the form he, ξi+1 , ϕ, ρi, where ρ is inde‐ Exercises
pendent of ρi+1 , and furthermore, some other evaluation judgment has
85
an initial state that depends on ρi+1 . Formal‐parameter environments
cannot simply be mutated in place. Their optimization is the topic of
the next exercise.
(a) Rewrite the semantics of Impcore to use this new judgment form. En‐
sure that if it is necessary use some other environment and also to re‐
member ρ, that the new environment is pushed on top of the stack ρ::S .
In eval, the implementation of every proof rule that ends in the judgment
form he, ξ, ϕ, ρ :: Si ⇓ hv, ξ ′ , ϕ, ρ′ :: Si can be implemented by popping ρ
off the stack, doing some computation, and pushing ρ′ onto the stack. (It is
possible that ρ′ = ρ.) The computation in the middle may include pushes,
pops, and recursive calls to eval. And once ρ is popped off the stack, it is
used only to make ρ′ and not in any other way—so it is safe to compute ρ′ by
mutating ρ in place.
(b) Prove that if ρ′ = ρ, then the only copy of ρ is the one on top of the
stack. If ρ′ 6= ρ, then once ρ is popped off the stack, it is thrown away
and never used again. In particular, no environment ever needs to be
copied anywhere except on the stack; that is, every environment that
might ever be needed is present on the stack.
Use structural induction on a derivation of the evaluation judgment
he, ξ, ϕ, ρ :: Si ⇓ hv, ξ ′ , ϕ, ρ′ :: Si. The base cases are the rules that
have no evaluation judgments in the premises, such as the LıTERAL or
FORMALVAR rules. The induction steps are the rules that do have eval‐
uation judgments as premises, such as FORMALAſſıGN.
This lemma implies that the operation “pop ρ; push ρ′ ” can be replaced by the
operation “mutate ρ in place to become ρ′ .” In particular, ρ{x 7→ v} can be
implemented by mutating an existing binding; building a new environment
is not necessary. The mutation is safe only because the sole copy of ρ is on
top of the stack.
This theorem justifies my implementation of bindval, as referred to in Sec‐
tion 1.6.3. The stack is the C call stack.
31. Extending Impcore to work with unbound variables. Implement your solutions
to Exercise 16. Use your implementation to test the code you write to distin‐
guish the new semantics.
32. Passing parameters by reference. Change the Impcore interpreter to pass pa‐
rameters by reference instead of by value. For example, if a variable x is
passed to a function f, function f can modify x by assigning to a formal pa‐
rameter. If a non‐variable expression is passed as an argument to a func‐
tion, assignments to formal parameters should have no effect outside the
function. (In particular, it should not be possible to change the value of an
integer literal by assignment to a formal parameter.)
To implement this change, change the return type of eval and fetchval to
be Value*, and make Valuelists hold Value*s rather than Values. Type
checking in your C compiler should help you find the other parts that need
to change. No change in syntax is needed.
Explore your implementation by writing a function that uses call by refer‐
ence. A good candidate is a function that wants to return multiple values,
like a division function that wants to return both quotient and remainder.
Then address these questions:
34. Adding new concrete syntax. Using syntactic sugar, extend Impcore with the
looping constructs discussed in Section 1.8:
35. Recovering lost file descriptors. The implementation of use in chunk hevaluate
d‑>use, possibly mutating globals and functions S296ci leaks open file de‐
scriptors when files have bugs. Explain how you would fix the problem.
36. Profiling. Write an Impcore program that takes a long time to execute. Profile
the interpreter.
Scheme combines power and simplicity. Scheme is derived from Lisp, which John
McCarthy developed—inspired in part by Alonzo Church’s work on the λcalculus—
while exploring ideas about computability, recursive functions, and models of
computation. McCarthy intended Lisp for computing with symbolic data he called
Sexpressions. S‐expressions are based on lists, and the name “Lisp” was formed
from “list processing.” Lisp programs can be concise and natural programs, and
they often resemble mathematical definitions of the functions they compute. Lisp
has been used heavily in artificial intelligence for over fifty years, and in 1971,
McCarthy received ACM’s Turing Award for contributions to artificial intelligence.
Lisp spawned many successor dialects, of which the most influential have been
Common Lisp and Scheme. Common Lisp was designed to unify many of the di‐
alects in use in the 1980s; its rich programming environment has attracted many
large software projects. Scheme was designed to be small, clean, and powerful;
its power and simplicity have attracted many teachers and authors like me.
Scheme was created by Guy Steele and Gerry Sussman, who introduced the
main ideas in a classic series of MIT technical reports in the late 1970s, all bearing
titles of the form “LAMBDA: The Ultimate (blank).” And Scheme may have been
made famous by Abelson and Sussman (1985), who show off its ability to express
many different programming‐language ideas and to build programs in many dif‐
ferent styles.
So what is Scheme? To answer such a question, set aside the syntax; the essence
of a language lies in its values. If the essence of C is pointer arithmetic, the essence
of Perl is regular expressions, and the essence of Fortran is arrays, the essence of
Scheme is lists and functions.1
1
Today, any list of Scheme’s essential aspects would also include hygienic macros. But hygienic
macros were developed relatively late, in the 1980s and 1990s, well after the other foundations of Scheme
were laid down. And unlike those foundations, macros have not colonized other languages. In this book,
macros, substitution, and hygiene are just barely touched on.
89
Lists come from original Lisp. Lists can contain other lists, so they can be used
to build records and trees. Lists of key‐value pairs can act as tables. Add numbers
and symbols, and lists provide everything you need for symbolic computation.
In addition to lists, Scheme provides first‐class, higher‐order, nested functions.
2 Nested functions are created at run time by evaluating lambda expressions, which
should dramatically change our thinking about programming and computation.
Scheme was meant to be small, but full Scheme is still too big for this book.
Scheme,
Instead, we use µScheme (pronounced “micro‐Scheme”), a distillation of Scheme’s
Sexpressions, and
essential features. In this book, “Scheme” refers to ideas that Scheme and µScheme
firstclass functions
share. “Full Scheme” and “µScheme” refer to the large and small languages, respec‐
90 tively.
Scheme shares some central ideas with Impcore, which you know:
• Scheme has simple, regular syntax. It has no infix operators and therefore
no operator precedence.
• µScheme makes it easy to define local variables, which are introduced and
initialized by let expressions.
The addition of nested functions, with the ability of one function to change an en‐
closing function’s variables, requires a change in the semantics:
• S‐expressions and other values are formed according to four simple rules
(Section 2.2).
Scheme’s values include not only integers but also Booleans, functions, symbols, and
lists of values, all of which are described below. The ones that can easily be written
down—the ones that don’t involve functions—are called Sexpressions, which is short
for “symbolic expressions.”
Scheme’s least familiar sort of value is the symbol; a symbol is a value that is a
name. To paraphrase Kelsey, Clinger, and Rees (1998), what matters about symbols
is that two symbols are identical if and only if their names are spelled in the same
way. That is, symbols behave like Name values from the Impcore interpreter. And
2 like Names, symbols are often used to represent identifiers in programs. They are
also used in the same way that enumeration literals are used in C, C++, or Java.
The other forms of µScheme value are more familiar: numbers, Booleans, lists,
Scheme,
and functions. They are described by three inductive rules, which are summarized
Sexpressions, and
in Figure 2.1 on the previous page:
firstclass functions
1. A symbol is a value, and so is a number. The Boolean values #t and #f are
92
values; #t and #f, not 1 and 0, canonically represent truth and falsehood.
Values defined by this rule are atomic: like atoms in the ancient Greek theory
of matter, they have no observable internal structure and cannot be “taken
apart.” Atomic values are also called atoms.
Although rules 1 to 3 cover the common cases, µScheme provides one more form
of value, which can be explained only by referring to µScheme’s cons primitive,
which is described below.
Values formed by rules 1 and 2 are ordinary Sexpressions, usually called just
“S‐expressions.” Examples might include the following:
3 10 ‑39 44 ; numbers
#t #f ; Booleans
hello frog ; symbols
(80 87 11) ; list of numbers
(frog newt salamander) ; list of symbols
(10 lords a‑leaping) ; list of mixed values
((9 ladies dancing) (8 maids a‑milking)) ; list of S‑expressions
• Primitives shared with Impcore include +, ‑, *, /, <, and >, which implement
arithmetic and comparisons. The comparisons return Booleans, not num‐
bers: #t if the condition holds, and otherwise, #f. Applying any of these
functions to a non‐number is a checked run‐time error, as is division by zero.
Primitives that are new with µScheme include type predicates, which are used to
identify a value’s form.
• Each type predicate, symbol?, number?, boolean?, null?, or function?, re‐
turns #t if its argument is of the named form, #f otherwise. Predicates
def ::= (val variablename exp) §2.2
| exp Language I:
| (define functionname (formals) exp) Values, syntax,
⋆ | (record recordname [ fieldname ]) and initial basis
| (use filename)
| unittest 93
numeral ::= token composed only of digits, possibly prefixed with a plus
or minus sign
*name ::= token that is not a bracket, a numeral, or one of the “re‐
served” words shown in typewriter font
Tokens are as in Impcore, except that if a quote mark ' occurs at the beginning of
a token, it is a token all by itself; e.g., 'yellow is two tokens.
Each quoted S‐expression (Sexp) is converted to a literal value by the parser. And
each record definition is expanded to a sequence of true definitions, also by the
parser; in other words, a record definition is syntactic sugar (Section 1.8,
page 68), as marked by the ⋆. Five forms of conditional expression are also
syntactic sugar. All the other forms are handled by the eval function.
2 • Function cons adds one element to the front of a list. If vs (pronouced “veez”)
is the empty list, then (cons v vs ) is the singleton list (v ). If vs is the non‐
empty list (v1 · · · vn ), then (cons v vs ) is the longer list (v v1 · · · vn ).
Scheme,
Sexpressions, and In Scheme, cons has a quirk not common in other functional languages: it can be
firstclass functions applied to any two values, not just to an element and a list. This quirk demands a
fourth rule for the formation of µScheme values:
94
4. If v1 and v2 are values, then (cons v1 v2 ) produces a value.
Scheme values formed by rules 1, 2, and 4 are called fully general Sexpressions.
Calling (cons v1 v2 ) always produces a value, but the result a list of values if and
only if v2 is a list of values. Values made with cons are identified by type predicate
pair?.
Primitives car and cdr can safely be applied to any value made with cons.
The name cons stands for “construct,” which makes some kind of sense. The
names car and cdr, by contrast, stand for “contents of the address part of regis‐
ter” and “contents of the decrement part of register,” which make sense only if we
are thinking about the machine‐language implementation of Lisp on the IBM 704.
In the Racket dialect of Scheme, car and cdr are called by the more sensible names
first and rest (Felleisen et al. 2018).
µScheme includes an equality primitive that works on more than just numbers:
• Function = tests atoms for equality. Calling (= v1 v2 ) returns #t if v1 and v2
are the same atom. That is, they may be the same symbol, the same number,
or the same Boolean, or they may both be the empty list. Given any two val‐
ues that are not the same atom, including any functions or nonempty lists,
(= v1 v2 ) returns #f. (To compare nonempty lists, we use the non‐primitive
function equal?, which is shown in chunk 104a.)
3
If you already know Scheme, or if you learn Scheme, you’ll notice some differences. For example,
in full Scheme, functions are called “procedures.” S‐expressions are called “datums.” The syntax of the
define form is different, and the val form uses the define keyword. The = primitive works only on
numbers, and it is complemented by other equality functions like eq?, eqv?, and equal?. Full Scheme’s
and and or forms are syntax, not functions, so they short‐circuit, like µScheme’s && and ||.
Last, the printing primitives are like Impcore’s printing primitives. Primitives
print and println work with any value.
As in Impcore, only println, print, and printu have side effects; other primi‐
tives compute new values without changing anything. For example, applying cons,
car, or cdr to a list does not change the list.
The primitives that µScheme shares with Impcore are demonstrated in Chap‐
ter 1. The list primitives are demonstrated below, by applying them to values
that are written as literals. Literals include numerals, Boolean literals, and quoted
S‐expressions (Figure 2.2, page 93). Using quoted S‐expressions, we can demon‐
strate cons, car, and cdr:
95a. htranscript 95ai≡ 95b ▷
‑> (cons 'a '())
(a)
‑> (cons 'a '(b))
(a b)
‑> (cons '(a) '(b))
((a) b)
‑> (cdr '(a (b (c d))))
((b (c d)))
‑> (car '(a (b (c d))))
a
Primitive null? finds that the empty list is empty, but it finds that a singleton list
containing the empty list is not empty:
95b. htranscript 95ai+≡ ◁ 95a 98 ▷
‑> (null? '())
#t
‑> (null? '(()))
#f
a b c
The car or cdr of a nonempty list is found by following the arrow that leaves
the left or right box of the first cons cell. As another example, the S‐expression
(a (b (c d))) is drawn like this:
2
a
Scheme,
Sexpressions, and b
firstclass functions
c d
96
Its cdr is simply what the first cell’s right arrow points at, which is, as above,
((b (c d))):
c d
When complex structures are built from cons cells, car and cdr are often ap‐
plied several times in succession. Such applications have traditional abbreviations:
96a. hpredefined µScheme functions 96ai≡ 96b ▷
(define caar (xs) (car (car xs)))
(define cadr (xs) (car (cdr xs)))
(define cdar (xs) (cdr (car xs)))
These definitions appear in chunk hpredefined µScheme functions 96ai, from which
they are built into the µScheme interpreter itself and are evaluated when the in‐
terpreter starts. Definitions are built in for all combinations of car and cdr up to
depth five, ending with cdddddr, but the others are relegated to the Supplement.
If applying car or cdr several times in succession is tiresome, so is applying
cons several times in succession. Common cases are supported by more prede‐
fined functions:
96b. hpredefined µScheme functions 96ai+≡ ◁ 96a 99b ▷
(define list1 (x) (cons x '()))
(define list2 (x y) (cons x (list1 y)))
(define list3 (x y z) (cons x (list2 y z)))
More cases, for list4 to list8, are defined in the Supplement. In full Scheme,
all possible cases are handled by a single, variadic function, list, which takes any
number of arguments and returns a list containing those arguments (Exercise 56).
Three predefined functions are similar but not identical to functions found in
Impcore: the Boolean functions and, or, and not. Instead of Impcore’s 1 and 0, they
return Boolean values.
96c. hdefinitions of predefined µScheme functions and, or, and not 96ci≡
(define and (b c) (if b c b))
(define or (b c) (if b b c))
(define not (b) (if b #f #t))
Functions and and or inconveniently evaluate both arguments, even when the first
argument determines the result. To evaluate only as much as is needed to make a
decision, use the syntactic forms && and || (Exercise 53).
Table 2.3: The initial basis of µScheme
In Scheme, a list is created in one of two ways: by using '() or cons. In the style of
the preceding section,
Like any other data type that can be created in multiple ways, a list is normally con‐
sumed by a function that begins with case analysis. Such a function distinguishes
cases using null?, and when the list is not null—it was created by using cons—a
typical function calls itself recursively on the rest of the list (the cdr).
One of the simplest such functions finds the length of a list of values:
98. htranscript 95ai+≡ ◁ 95b 99a ▷
‑> (define length (xs)
(if (null? xs)
0
(+ 1 (length (cdr xs)))))
The length function typifies recursive functions that consume lists: when it sees an
empty list, its recursion stops (the base case); when it sees a nonempty list, it calls
itself recursively on (cdr xs) (an induction step). The name xs (pronounced “exes”)
suggests a list of elements of unknown type; a single x suggests one such element.
The behavior of length can be summarized in two equations: one for each form
that its argument can take (forms L1 and L2 above).
(length '()) =0
(length (cons v vs )) = (+ 1 (length vs ))
These equations are algebraic laws (Section 2.5). Algebraic laws often tell us exactly
what to do with each possible form of input, making them a great way to plan an
implementation. In practice, algebraic laws are also used for specification, proof,
optimization, and testing.
The algebraic laws for length specify exactly what length does; if you want to
use length and you understand the laws, you never need to look at the code. But if
it’s not obvious how the code works, it might help to look more closely at an example
call. When (length '(a b)) is evaluated, here’s what happens:
• The list (a b) is not the empty list, so (null? xs) returns #f. §2.3
Practice I:
• The expression (+ 1 (length (cdr xs))) is evaluated, where xs = (a b). Call‐
Recursive
ing (cdr xs) returns list (b). When length is applied to (b),
functions on
– List xs = (b). lists of values
– List (b) is not the empty list, so (null? xs) returns #f. 99
– Expression (+ 1 (length (cdr xs))) is evaluated. Calling (cdr xs) re‐
turns the empty list. When length is applied to the empty list,
* List xs = ().
* (null? xs) returns #t.
* length returns 0.
– The call (length (cdr xs)) returns 0, so length returns 1.
As another example of recursive code specified using algebraic laws, let’s look
at the predefined function append. Function append takes two lists, xs and ys , and
it returns a list that contains the elements of xs followed by the elements of ys :
99a. htranscript 95ai+≡ ◁ 98 100a ▷
‑> (append '(moon over) '(miami vice))
(moon over miami vice)
Interestingly, append never looks at ys ; it inspects only xs . And like any list,
xs is formed using either '() or cons. If xs is empty, append returns ys . If xs
is (cons z zs ), append returns z followed by zs followed by ys . The behavior of
append can be specified precisely using two algebraic laws:
(append '() ys ) = ys
(append (cons z zs ) ys ) = (cons z (append zs ys ))
cdr P 162a
2.3.2 List reversal and the method of accumulating parameters null? P 162a
Algebraic laws can also help us design a list‐reversal function—and make it effi‐
cient. To clarify the design, I condense the notation used to write laws, eliminating
keywords and parentheses:
ϵ · ys = ys
(z · zs) · ys = z · (zs · ys)
2 And the reversal laws look like this:
Scheme, R(ϵ) =ϵ
Sexpressions, and R(z · zs) = R(zs) · z
firstclass functions
Translated back to Scheme, the reversal laws are
100
(simple‑reverse '()) = '()
(simple‑reverse (cons z zs)) = (append (simple‑reverse zs) (list1 z))
This simple‑reverse function is expensive: append takes O(n) time and space,
and so simple‑reverse takes O(n2 ) time and space, where n is the length of the
list. But list reversal can be implemented in linear time. In Scheme, reversal is
made efficient by using a trick: take two lists, xs and ys , and return the reverse
of xs , followed by (unreversed) ys . List xs is either empty or is z followed by zs ,
and the computation obeys these laws:
R(ϵ) · ys = ys
R(z · zs) · ys = (R(zs) · z) · ys = R(zs) · (z · ys)
(revapp '() ys ) = ys
(revapp (cons z zs ) ys ) = (revapp zs (cons z ys ))
Function revapp takes time and space linear in the size of xs. Using it with an empty
list makes predefined function reverse equally efficient.
100c. hpredefined µScheme functions 96ai+≡ ◁ 100b 103a ▷
(define reverse (xs) (revapp xs '()))
In the code, if sorted is (cons k ks ), then k is (car sorted) and ks is (cdr sorted).
101a. htranscript 95ai+≡ ◁ 100d 101b ▷
‑> (define insert (m sorted)
(if (null? sorted)
(list1 m)
(if (< m (car sorted))
(cons m sorted)
(cons (car sorted) (insert m (cdr sorted))))))
As another example of a recursive function that uses the values of list elements,
I implement a well‐known algorithm for finding prime numbers. The algorithm
starts with a sequence of numbers from 2 to n, and it produces a prime p by taking
the first number in the sequence, then continuing recursively after removing all
multiples of p. Because it identifies a multiple of p by trying to divide by p, the
algorithm is called trial division.4
The sequence of numbers from 2 to n is created by (seq 2 n); in general,
(seq m n) returns a list containing numbers m, m + 1, m + 2, . . . , n. A multi‐
4
This algorithm is sometimes called the Sieve of Eratosthenes, but don’t be fooled: O’Neill (2009) will
convince you that this algorithm is not what Eratosthenes had in mind.
2.3.5 Coding with Sexpressions: Lists of lists
Even more recursion happens when a list element is itself a list, which can contain
other lists, and so on. Such lists, together with the atoms (rule 1, page 92), constitute
the ordinary Sexpressions.
§2.3
An ordinary S‐expression is either an atom or a list of ordinary S‐expressions.5
Practice I:
An atom is identified by predefined function atom?:
Recursive
103a. hpredefined µScheme functions 96ai+≡ ◁ 100c 104a ▷
functions on
(define atom? (x)
lists of values
(or (symbol? x) (or (number? x) (or (boolean? x) (null? x)))))
These laws work with fully general S‐expressions, which can be formed using value
rule 4, not just rules 1 and 2 (Figure 2.1, page 91). That’s why, in the law, the cons
cell is written (cons y z ) and not (cons y ys ).
In the code, if sx is (cons y z ), then y is (car sx) and z is (cdr sx).
103b. htranscript 95ai+≡ ◁ 102d 103c ▷
‑> (define has? (sx a)
(if (atom? sx)
(= sx a)
(or (has? (car sx) a) (has? (cdr sx) a))))
This code calls has? twice. It could be made faster by using an if expression instead
of or, or by using short‐circuit operator || (Section 2.13.3, page 164).
Function has? can search a list of lists of symbols:
103c. htranscript 95ai+≡ ◁ 103b 104b ▷ car P 162a
‑> (val pangrams ;; www.rinkworks.com/words/pangrams.shtml, June 2018 cdr P 162a
'((We promptly judged antique ivory buckles for the next prize.) cons P S313d
(The quick red fox jumps over a lazy brown dog.) mod B
null? P 162a
(Amazingly few discotheques provide jukeboxes.)
or B
(Heavy boxes perform quick waltzes and jigs.)
(Pack my box with five dozen liquor jugs.)))
‑> (has? pangrams 'fox)
#t
‑> (has? pangrams 'box)
#t
‑> (has? pangrams 'cox)
#f
5
The empty list '() is both an atom and a list of ordinary S‐expressions.
2.3.6 Inspecting multiple inputs: Equality on Sexpressions
Functions like length, append, insert, and has? inspect only one list or one
S‐expression. A function that inspects two S‐expressions must prepare for all forms
2 of both inputs, for a total of four cases. As an example, function equal? compares
two S‐expressions for equality—they are equal if they are formed from the same
atoms in the same way. Breaking the inputs down by cases, two atoms are equal
Scheme,
if they are the same, as tested with primitive =. Two lists are equal if they contain
Sexpressions, and
(recursively) equal elements in equal positions. An atom and a nonempty list are
firstclass functions
never equal.
104
(equal? sx 1 sx 2 ) = (= sx 1 sx 2 ),
if sx 1 is an atom and sx 2 is an atom
(equal? sx 1 (cons w z ))) = #f, if sx 1 is an atom
(equal? (cons x y ) sx 2 ) = #f, if sx 2 is an atom
(equal? (cons x y ) (cons w z )) = (and (equal? x w ) (equal? y z ))
These laws call for four cases, but in an implementation, the first two laws can be
combined: the second law calls for equal? to return #f, but when sx 1 is an atom
and sx 2 is (cons w z ), (= sx 1 sx 2 ) always returns false, so both cases where sx 1 is
an atom may use =:
104a. hpredefined µScheme functions 96ai+≡ ◁ 103a 106a ▷
(define equal? (sx1 sx2)
(if (atom? sx1)
(= sx1 sx2)
(if (atom? sx2)
#f
(and (equal? (car sx1) (car sx2))
(equal? (cdr sx1) (cdr sx2))))))
More rigorous testing confirms both #f and (when possible) #t results for all four
cases.
In Scheme, as in any other language, a list with no repeated elements can repre‐
sent a set. As long as the set is small, this representation is efficient, and the set
operations are easy to write and to understand. Operations emptyset, member?,
add‑element, size, and union are shown below; their algebraic laws are shown in
comments. Other operations appear at the end of the chapter (Exercise 3).
Function member? requires explicit recursion.
105a. htranscript 95ai+≡ ◁ 104b 105b ▷
‑> (val emptyset '())
‑> (define member? (x s) ; (member? x '()) = #f
(if (null? s) ; (member? x (cons x ys)) = #t
§2.3
#f ; (member? x (cons y ys)) = (member? x ys),
(if (equal? x (car s)) ; when x differs from y
Practice I:
#t Recursive
(member? x (cdr s))))) functions on
lists of values
Function add‑element might be implemented recursively, but instead it calls
member?. 105
105b. htranscript 95ai+≡ ◁ 105a 105c ▷
‑> (define add‑element (x s) ; (add‑element x s) = xs, when x is in s
(if (member? x s) ; (add‑element x s) = (cons x xs),
s ; when x is not in s
(cons x s)))
‑> (val s (add‑element 3 (add‑element 'a emptyset)))
(3 a)
‑> (member? 'a s)
#t
Because member? tests for identity using equal?, it can recognize a list as an
element of a set:
105d. htranscript 95ai+≡ ◁ 105c 106d ▷
‑> (val t (add‑element '(a b) (add‑element 1 emptyset)))
((a b) 1)
‑> (member? '(a b) t)
#t
If member? used = instead of equal?, this last example wouldn’t work; I encourage
you to explain why (Exercise 4). car P 162a
cdr P 162a
2.3.8 Association lists cons P S313d
null? P 162a
A list of ordered pairs can represent a classic data structure of symbolic computing:
the finite map (also called associative array, dictionary, and table). Finite maps are
ubiquitous; for example, in this book they are used to represent the environments
found in operational semantics and in interpreters. (In an interpreter or compiler,
an environment is often called a symbol table.)
A small map is often represented as an association list. An association list
has the form ((k1 a1 ) · · · (km am )), where each ki is a symbol, called a key,
and each ai is an arbitrary value, called an attribute. A pair (ki ai ) is made
with function make‑alist‑pair and inspected with functions alist‑pair‑key and
alist‑pair‑attribute:
S‐expressions can code all forms of lists and trees: a lot of structured data. But
when all data are S‐expressions, every interesting structure is made with cons cells.
Such structures are traversed with combinations of car and cdr, and using car and
cdr for everything is too much like programming in assembly language.
Functions like car and cdr are perfect for data whose size or structure may vary.
But many data structures store a fixed number of elements in known locations,
like a C struct. In Scheme, such structures can be represented by records. In this
section, records are introduced by example, then used to represent binary trees.
2.4.1 Records
• A protein
• A starch
• A vegetable
• A dessert
• As in C, each field is accessed by using its name. But where C uses dot
notation, writing .starch after a struct (or quite commonly, ‑>starch
after a pointer to a struct), µScheme uses an accessor function, applying
frozen‑dinner‑starch to a record.
µScheme records are allocated and initialized differently from C structs. A struct
is allocated by malloc (or in C++, new), and each individual field is initialized by
an individual assignment. A µScheme record is allocated and initialized by a con
structor function, which receives the initial values of all the fields as its arguments.
2 The constructor function is a bit like a constructor in C++ or Java, except its be‐
havior is determined by the language and cannot be changed by the programmer.
The constructor function expects one argument per field of the record, in the order
Scheme,
in which they appear, as follows:
Sexpressions, and
firstclass functions 108a. htranscript 95ai+≡ ◁ 107b 108b ▷
‑> (make‑frozen‑dinner 'steak 'potato 'green‑beans 'pie)
108 (make‑frozen‑dinner steak potato green‑beans pie)
‑> (make‑frozen‑dinner 'beans 'rice 'tomatillo 'flan)
(make‑frozen‑dinner beans rice tomatillo flan)
‑> (frozen‑dinner‑starch it)
rice
When a record form is evaluated, the interpreter prints the names of the func‐
tions that it defines:
108c. htranscript 95ai+≡ ◁ 108b 108d ▷
‑> (record frozen‑dinner [protein starch vegetable dessert])
make‑frozen‑dinner
frozen‑dinner?
frozen‑dinner‑protein
frozen‑dinner‑starch
frozen‑dinner‑vegetable
frozen‑dinner‑dessert
A short name like .protein won’t work in Scheme, because there is no type
system to tell us which record is meant. Long names, like nutrition‑protein
and frozen‑dinner‑protein, say explicitly which record is meant (nutritional‐
information records and frozen‐dinner records, respectively).
6
Not quite: In µScheme, but not in full Scheme, the type predicate can be fooled by a record that is
forged using cons (Section 2.13.6).
Functions defined by record satisfy algebraic laws, which say that we get out
what we put in:
Records, like C structs, make great tree nodes. For example, in a binary tree,
a node might contain a tag and two subtrees. A binary tree is either such a node or
is empty. A node can be represented as a node record, and the empty tree can be
represented as #f.
109a. htranscript 95ai+≡ ◁ 108d 109b ▷
‑> (record node [tag left right])
make‑node
node?
node‑tag
node‑left
node‑right
The set of tagged binary trees can be defined precisely, by induction. The set
BINTREE (T ) contains tagged binary trees with tags drawn from set T :
#f ∈ BINTREE (T )
B E
2 C D F I
G H
Scheme,
Sexpressions, and Tagged binary trees are consumed by functions whose internal structure fol‐
firstclass functions lows the structure of the input. Just as a list‐consuming function must handle
110 two forms, cons and '(), a tree‐consuming function must handle two forms:
make‑node and empty. The forms can be distinguished by predicate empty‑tree?:
110a. htranscript 95ai+≡ ◁ 109b 110b ▷
‑> (define empty‑tree? (tree) (= tree #f))
Inorder and postorder traversals are left for you to implement (Exercise 11).
Throughout this chapter, functions are described by algebraic laws. The laws help
us understand what the code does (or what it is supposed to do). Algebraic laws also
help with design: they can show what cases need to be handled and what needs to
be done in each case. Translating such laws into code is much easier than writing
correct code from scratch.
Algebraic laws have many other uses. Any algebraic law can be turned into a
test case by substituting example data for metavariables; for example, QuickCheck
(Claessen and Hughes 2000) automatically substitutes a random input for each
metavariable. Algebraic laws are also used to specify the behavior of abstract types,
to simplify code, to improve performance, and even to prove properties of code.
Algebraic laws work by specifying equalities: in a valid law, whenever values
are substituted for metavariables, the two sides are equal. (The values substituted
must respect the conditions surrounding the law. For example, if ns stands for a
list of numbers, we may not substitute a Boolean for it.) The substitution principle
extends beyond values; a valid law also holds when program variables are substi‐
tuted for metavariables, and even when pure expressions are substituted for meta‐
variables. A pure expression is one whose evaluation has no side effects: it does
not change the values of any variables and does not do any input or output. And
for our purposes, a pure expression runs to successful completion; if an expres‐
sion’s evaluation doesn’t terminate or triggers a run‐time error, the expression is
considered impure.
The equality specified by an algebraic law is a form of observational equivalence:
if e1 = e2 , and a program contains e1 , we can replace e1 with e2 , and the program
won’t be able to tell the difference. That is, running the altered program will have
the same observable effect as the original. Replacing e1 with e2 may, however,
change properties that can’t be observed by the program itself, such as the time
required for completion or the number of locations allocated. When an algebraic
law is used to improve performance, changing such properties is the whole point.
In the sections above, algebraic laws are used only to show how to implement
§2.5
functions. In this section, they are also used to specify properties of functions and
Combining theory
of combinations of functions, and to prove such properties.
and practice:
Algebraic laws
2.5.1 Laws of list primitives
111
Algebraic laws can be used to specify the behaviors of primitive functions. After
all, programmers never need to see implementations of cons, car, cdr, and null?;
we just need to know how they behave. Their behavior can be specified by oper‐
ational semantics, but operational semantics often gives more detail than we care
to know. For example, if we just want to be able to use car and cdr effectively,
everything we need to know is captured by these two laws:
(car (cons x y )) =x
(cdr (cons x y )) = y
These laws also tell us something about cons: implicitly, the laws confirm that cons
may be applied to any two arguments x and y , even if y is not a list (rule 4, page 91).
Use of cons cells and '() to represent lists is merely a programming convention.
To capture cons completely also requires laws that tell us how a cons cell is
viewed by a type predicate, as in these examples:
(pair? (cons x y )) = #t
(null? (cons x y )) = #f
The laws above suffice to enable us to use cons, car, and cdr effectively. Noth‐
ing more is required, and any implementation that satisfies the laws is as correct
as any other. To develop an unusual implementation, try Exercise 39.
How many laws are enough? To know if we have enough laws to describe a data
type T , we analyze the functions that involve values of type T .
append B 99
• A function that makes a new value of type T is a creator or a producer. A cre‐ cons P S313d
example‑sym‑tree
ator is either a value of type T all by itself, or it is a function that returns a 109b
value of type T without needing any arguments of type T . As an example, node‑left 109a
'() is a creator for lists. A producer is a function that takes at least one ar‐ node‑right 109a
node‑tag 109a
gument of type T , and possibly additional arguments, and returns a value of
type T . As an example, cons is a producer for lists.
Creators and producers are sometimes grouped into a single category called
constructors, but “constructor” is a slippery word. The grouping usage comes
from algebraic specification, but “constructor” is also used in functional pro‐
gramming and in object‐oriented programming—and in each community,
it means something different.
2 • Creators, producers, and observers have no side effects. A function that has
side effects on an existing value of type T is a mutator. Mutators, too, can
fit into the discipline of algebraic specification. An explanation in depth is
Scheme, beyond the scope of this book, but a couple of simple examples appear in Sec‐
Sexpressions, and tion 9.6.2 (page 547). For more, see the excellent book by Liskov and Guttag
firstclass functions (1986).
112 This classification of functions tells us how many laws are enough: there are
enough laws for type T if the laws specify the result of every permissible combina‐
tion of observers applied to creators and producers. By this criterion, our list laws
aren’t yet complete; they don’t specify what happens when observers are applied
to the empty list. Such observations are specified by these laws:
(pair? '()) = #f
(null? '()) = #t
Not all observations of the empty list are permissible. An observation would cause
an error, like (car '()), isn’t specified by any law, and so it is understood that the
observation is impermissible. This convention resembles the convention of the
operational semantics, where if an evaluation causes an error, no rule applies.
Laws for rich data structures can be extensive. Because S‐expressions include
both lists and atoms, they have lots of creators, producers, and especially observers.
Laws for all combinations would be overwhelming; only a few Boolean laws are
sketched below.
For the Booleans, values #t and #f act as creators, the syntactic form if acts like
an observer, and the predefined function not is a producer. Their interactions are
described by these laws:
(if #t x y ) =x
(if #f x y ) =y
(if (not p) x y ) = (if p y x)
(if p #f #t) = (not p)
(if p #t #f) =p
This law, which is frequently overlooked, is also valid if the result of the if ex‐
pression is used only as a condition in other if expressions (or while expressions).
Whenever possible, it should be used to simplify code.
Laws for association lists (Section 2.3.8) tell us that find returns the most recent
property added with bind:
(and p q )
= (and q p)
(and p #t)=p
(and p #f) = #f
(or p q ) = (or q p)
(or p #t) = #t
(or p #f) = p
(append (cons x '()) xs ) = (cons x xs )
(append (append xs ys ) zs ) = (append xs (append ys zs ))
(reverse (reverse xs )) = xs
These laws, which I often use in my own programming, are great for simplify‐
ing code. They also make excellent laws for “property‐based” testing (Claessen and
Hughes 2000).
Algebraic laws enable a new, elegant form of proof. What good is a new form of
proof? It proves more interesting facts with less work then operational semantics.
In principle, anything we might prove about a µScheme program can be proved
using the operational semantics. But using operational semantics to prove prop‐
erties of programs is like using assembly language to write them: it operates at so
low a level that it’s practical only for small problems. Operational semantics is best
used only to prove laws about primitive functions and syntactic forms. Those laws
can then be used to prove laws about functions, which can be used to prove laws
about other functions, and so on. In proof, laws play a similar role to the role that
functions play in coding: they enable us to break problems down hierarchically.
These hierarchical proofs are founded on proofs about primitives and syn‐
tax. Unfortunately, the foundational proofs can be quite challenging; for exam‐
ple, although no program can tell the difference between the two expressions
(let ([x 1983]) x) and just 1983, they do not have exactly the same semantics:
one allocates a fresh location and the other doesn’t. The extra location can’t be
observed, but to prove it requires techniques that are far beyond the scope of this
book.
What we do in this book is freely substitute one pure expression for another,
provided they always evaluate to equal values—even if they don’t have identical ef‐
fects on the store. This substitution of equals for equals is a simple, powerful proof
technique, and it works not just on primitives and simple syntactic forms, but also
on function applications. To substitute for a function application, we expand the
body of the function by replacing each formal parameter with the corresponding
actual parameter. As long as there are no side effects, this too is substitution of
equals for equals.
Substitution is justified by algebraic laws: an algebraic law says, “these two
sides are equal, and so one may be freely substituted for the other.” This tech‐
2 nique is called equational reasoning, and the resulting proofs are sometimes called
calculational proofs. It is demonstrated in the next section.
Scheme,
Sexpressions, and 2.5.7 Using equational reasoning to write proofs
firstclass functions
Equational reasoning can be used to prove the laws for append and length given
114 in Section 2.3.1. The first append law on page 99 says that (append '() ys ) = ys .
To prove it, I first substitute for append’s formal parameters, then apply the “null‐
empty” law from Section 2.5.2 and the “if‐true” law from Section 2.5.3:
(append '() ys )
= {substitute actual parameters '() and ys in definition of append}
(if (null? '())
ys
(cons (car '()) (append (cdr '()) ys )))
= {null‐empty law}
(if #t
ys
(cons (car '()) (append (cdr '()) ys )))
= {if‐#t law}
ys
term 1
= { justification that term 1 = term 2 }
term 2
These steps are chained together to show that every term is equal to every other
term, and in particular that the first term is equal to the last term. In the example
above, the chain of equalities establishes that (append '() ys ) = ys . The append‐
cons law, (append (cons z zs ) ys ) = (cons z (append zs ys )), is proved in simi‐
lar fashion.
The key step in the proof is the expansion of the definition of append. As an‐
other example of such an expansion, I prove that the length of a cons cell is one
more than the length of the second argument:
Again the expansion is the first step: in the definition of length, the actual param‐
eter (cons y ys ) is substituted for the formal parameter xs:
(length (cons y ys ))
= {substitute actual parameter in definition of length}
(if (null? (cons y ys ))
0
(+ 1 (length (cdr (cons y ys )))))
= {null?‐cons law}
(if #f
0
(+ 1 (length (cdr (cons y ys )))))
= {if‐#f law}
(+ 1 (length (cdr (cons y ys ))))
§2.5
= {cdr‐cons law}
Combining theory
(+ 1 (length ys ))
and practice:
Expanding a function’s definition works, but it can make a proof long, verbose, Algebraic laws
and hard to follow. A function’s definition should be expanded as little as possible—
115
just enough to prove the laws that describe its implementation. Then, in future
proofs, only those laws are needed. As an example of using such a law, I prove that
appending a singleton list is equivalent to cons. I still substitute actual parameters,
but now instead of substituting them for the formal parameters of append, I sub‐
stitute them for the metavariables of the append‐cons law on the preceding page.
The key step is again the first step, where I substitute x for z and '() for zs , leaving
ys unchanged:
The examples above prove properties about the application of append or length
to a list that is known to be short (empty or singleton). But useful properties aren’t
limited to short lists; for example, appending two lists adds their lengths, even
when the first argument to append is arbitrarily long. The computation involves
a recursive call to append, and proving general facts about recursive functions usu‐
ally demands proof by induction. In particular, laws about lists and S‐expressions
are proved by structural induction:
• Prove the law holds for every base case. In the case of a list, prove that the
law holds when the list is empty.
• Prove every induction step by assuming the law holds for the constituents.
Again in the case of a list, prove the case for a nonempty list (cons z zs ) by
assuming the induction hypothesis for the smaller list zs .
(length (append xs ys ))
2 = {by assumption that xs is not empty, xs = (cons z zs )}
(length (append (cons z zs ) ys ))
Scheme, = {appeal to the append‐cons law}
Sexpressions, and (length (cons z (append zs ys )))
firstclass functions = {appeal to the length‐cons law}
116 (+ 1 (length (append zs ys )))
= {apply the induction hypothesis}
(+ 1 (+ (length zs ) (length ys )))
= {associativity of +}
(+ (+ 1 (length zs )) (length ys ))
= {length‐cons law, from right to left}
(+ (length (cons z zs )) (length ys ))
= {by the initial assumption that xs = (cons z zs )}
(+ (length xs) (length ys ))
These examples should teach you enough so that you can do Exercises 16 to 26
starting on page 183.
Not all fully general S‐expressions can be written with µScheme’s quote form; for
example, the result of evaluating (cons 2 2) cannot be written with a quote.
Inductively defined data is so common that it’s useful to have a shorthand nota‐
tion for it. The most common notations, including the datatype definition forms
of Standard ML (Chapter 5) and µML (Chapter 8), are related to recursion equations.
For example, the simplest recursion equation for LIST (A) looks like this:
This equation can’t quite be taken as a definition, unless we say that LIST (A) is
the smallest set that satisfies the equation.
S‐expressions can be described by similar equations:
While a lot can be done with just formal parameters, big functions need local vari‐
ables. In a procedural language like C or Impcore, local variables can be introduced
without being initialized, and they are often assigned to more than once. But in a
functional language like Scheme, local variables are always initialized when intro‐
duced, and afterward, they are rarely assigned to again.
In Scheme, local variables are introduced by let binding. A let binding is of‐
ten hand‐written as “let x = e′ in e,” which means “evaluate e′ , let x stand for
the resulting value, and evaluate e.” In Scheme, the same binding is written
(let ([x e′ ]) e). In general, Scheme’s let form binds a collection of values:
This let expression is evaluated as follows: First evaluate the righthand sides
e1 through en ; call the results v1 , . . . , vn . Next, extend the local environment so
that x1 stands for v1 , and so on. Finally, in the extended environment, evaluate
the body e; the value of the body becomes the value of the entire let expression.
The let form helps us avoid repeating computation, and it helps make code
readable. To enhance its readability, I write its bindings in square brackets.
(In µScheme, in full Scheme, and in all the bridge languages, square brackets mean
the same as round brackets (“parentheses”). I typically use round brackets to wrap
expressions, definitions, and lists of formal parameters; I use square brackets to
wrap other kinds of syntax, like binding pairs or local‐variable declarations.)
A let form can be thought of as naming the result of a computation. As an exam‐
ple, let’s compute the roots of the quadratic equation:
√
ax2 + bx + c = 0. From
−b± b2 −4ac
the quadratic formula, the roots are x = 2a . Although this formula is
not much use without real numbers, it can still be implemented in µScheme, and
2 it can compute roots of such equations as x2 + 3x − 70 √= 0. Decent candidates to
be named with let are the values of subformulas −b, b2 − 4ac, and 2a:
Scheme, 118a. htranscript 95ai+≡ ◁ 110b 118b ▷
Sexpressions, and hdefinition of sqrt S318bi
firstclass functions ‑> (define roots (a b c)
(let ([minus‑b (negated b)]
118 [discriminant (sqrt (‑ (* b b) (* 4 (* a c))))]
[two‑a (* 2 a)])
(list2 (/ (+ minus‑b discriminant) two‑a)
(/ (‑ minus‑b discriminant) two‑a))))
‑> (roots 1 3 ‑70)
(7 ‑10)
In a let expression, all of the right‐hand sides are evaluated before any of the
xi ’s are bound. It is often more useful to evaluate and bind one expression at a time,
in sequence, so that right‐hand side ei can refer to the values named x1 , . . . , xi−1 .
Sequential binding is implemented by the let* form. This form has the same struc‐
ture as let, but after evaluating the first right‐hand side e1 , it extends the environ‐
ment by binding the result to x1 . Then it evaluates e2 in the extended environment,
binds the result to x2 , and so on.
The difference between let and let* can be illustrated by a contrived example:
118b. htranscript 95ai+≡ ◁ 118a 118c ▷
‑> (val x 'global‑x)
‑> (val y 'global‑y)
‑> (let
([x 'local‑x]
[y x])
(list2 x y))
(local‑x global‑x)
In this example, because the right‐hand sides in a let are evaluated in the original
environment, the x in [y x] refers to the global definition of x. Using let*, the same
structure works differently:
118c. htranscript 95ai+≡ ◁ 118b 119a ▷
‑> (val x 'global‑x)
‑> (val y 'global‑y)
‑> (let*
([x 'local‑x]
[y x])
(list2 x y))
(local‑x local‑x)
In this example, because the right‐hand sides in a let* are evaluated and bound in
sequence, the x in [y x] refers to the local definition of x.
Any let* expression can be simulated with a nested sequence of let expres‐
sions, but let* is more readable and more convenient. As evidence of let*’s utility,
I show a levelorder traversal (also called breadth‐first traversal) of a binary tree. This
traversal visits every node on one level before visiting any node on the next level.
In effect, it visits nodes in order of distance from the root. For example, level‐order
traversal of the tree on page 109 visits the nodes in the order (A B E C D F I G H).
Level‐order traversal uses an auxiliary data structure: a queue of nodes not yet
visited. Traversal starts with a queue containing only the root, and it continues until
the queue is empty. When the queue is not empty, the traversal visits the node at the
front of the queue, then enqueues the node’s children at the end. The implemen‐
tation needs queue operations emptyqueue, front, without‑front, and enqueue:
119a. htranscript 95ai+≡ ◁ 118c 119b ▷
‑> (val emptyqueue '())
‑> (define front (q) (car q))
‑> (define without‑front (q) (cdr q))
‑> (define enqueue (t q)
(if (null? q)
§2.6
(list1 t) Language II: Local
(cons (car q) (enqueue t (cdr q))))) variables and let
‑> (define empty? (q) (null? q))
119
This implementation of queues is woefully inefficient—it has the same cost as
append—but it’s simple. I encourage you to do better (Exercise 13) and also to de‐
scribe the queue’s behavior using algebraic laws (Exercise 14).
The traversal is performed recursively by auxiliary function level‑order‑of‑q,
which receives an initial queue that contains only the tree to be traversed. It uses
let* to bind the first element to the name hd, which is then used both to make a
new queue newq and to make the result in the body.
119b. htranscript 95ai+≡ ◁ 119a 119c ▷
‑> (define level‑order‑of‑q (queue)
(if (empty? queue)
'()
(let* ([hd (front queue)]
[tl (without‑front queue)]
[newq (if (empty‑tree? hd)
tl
(enqueue (node‑right hd)
(enqueue (node‑left hd) tl)))])
(if (node? hd)
(cons (node‑tag hd) (level‑order‑of‑q newq))
(level‑order‑of‑q newq)))))
‑> (define level‑order (t)
(level‑order‑of‑q (enqueue t emptyqueue))) car P 162a
‑> (level‑order example‑sym‑tree) cdr P 162a
cons P S313d
(A B E C D F I G H)
empty‑tree? 110a
The example code names three queues: queue, tl, and newq. Picking the wrong example‑sym‑tree
109b
one can cause an error; for example, if queue is used place of tl, the function will
list1 B 96
loop. In an idiomatic imperative function, such an error wouldn’t occur; the func‐ list2 B 96
tion would use just one variable, queue, whose value would change over time. In an negated B 650
idiomatic applicative function, the same error can also be avoided; by clever use node‑left 109a
node‑right 109a
of let*, the name queue can be repeatedly rebound so that it always refers to the node‑tag 109a
queue of interest: node? 109a
119c. htranscript 95ai+≡ ◁ 119b 120a ▷ null? P 162a
sqrt S318b
‑> (define level‑order‑of‑q (queue)
(if (empty? queue)
'()
(let* ([hd (front queue)]
[queue (without‑front queue)]
[queue (if (empty‑tree? hd)
queue
(enqueue (node‑right hd)
(enqueue (node‑left hd) queue)))])
(if (node? hd)
(cons (node‑tag hd) (level‑order‑of‑q queue))
(level‑order‑of‑q queue)))))
120a. htranscript 95ai+≡ ◁ 119c 120b ▷
‑> (define level‑order (t)
(level‑order‑of‑q (enqueue t emptyqueue)))
‑> (level‑order example‑sym‑tree)
2 (A B E C D F I G H)
Both the imperative and the applicative idioms accomplish the same goal: they use
Scheme, just one name, queue, and at each point in the program, it means the right thing.
Sexpressions, and The let and let* forms have a recursive sibling, letrec, which has yet a third
firstclass functions set of rules for the visibility of the bound names xi . In a let expression, none of
the xi ’s can be used in any of the ei ’s. In a let* expression, each xi can be used in
120 any ej with j > i, that is, an x can be used in all the e’s that follow it. In a letrec ex‐
pression, all of the xi ’s can be used in all of the ei ’s, regardless of order. The letrec
form is used to define recursive functions. A simple example appears on page 135,
but a detailed explanation is best deferred to the formal treatment of lambda and
closures (Section 2.11.2).
(lambda (x1 x2 · · · xn ) e)
denotes the function that takes values v1 , v2 , . . . , vn and returns the result of eval‐
uating e in an environment where x1 is bound to v1 , x2 is bound to v2 , and so on.
Together with the LET expressions, the lambda expression is what distinguishes
µScheme syntax from Impcore syntax; roughly speaking, µScheme is Impcore plus
LET expressions, LAMBDA expressions, and S‐expression data. The lambda expres‐
sion is the key to many useful programming techniques, the most important and
widely used of which are presented in this chapter.
By itself, lambda is not obviously powerful; one lambda is simple and innocuous.
For example, (lambda (x y) (+ (* x x) (* y y))) denotes the function that, given
values v and w, returns v 2 + w2 . Its only novelty is that it is anonymous; unlike
Impcore functions and C functions, Scheme functions need not be named.
120b. htranscript 95ai+≡ ◁ 120a 121a ▷
‑> ((lambda (x y) (+ (* x x) (* y y))) 3 4)
25
‑> ((lambda (x y) (+ (* x x) (* y y))) 707 707)
999698
‑> ((lambda (x y z) (+ x (+ y z))) 1 2 3)
6
‑> ((lambda (y) (* y y)) 7)
49
The lambda looks like a new way of defining functions, so you might think that
µScheme has two kinds of functions, one created with lambda and one created with
define. But define is an abbreviation—another form of syntactic sugar. A define
form can be desugared into a combination of val and lambda:
△
(define f (x1 · · · xn ) e) = (val f (lambda (x1 · · · xn ) e)).
As described in the sidebar on page 68, this kind of syntactic sugar is popular;
it gives programmers a nice big language to use, while keeping the core language
and its operational semantics (and the proofs!) small.
The define form can be desugared into a val only because in µScheme, a func‐
tion is just another kind of value—and all values, including functions, are bound in
§2.7
the same environment (Section 2.11). In Impcore, where functions and values are
Language III:
bound in different environments, define could not be desugared into a val.
Firstclass
Desugaring define into val is one way to exploit the idea that a function defined
functions, lambda,
with lambda is just another value. Besides putting it on the right‐hand side of a val,
and locations
what else can you do with such a function? What you can do with any other value:
121
• Pass it as an argument to a function.
• Return it from a function.
• Store it in a global variable, using set.
• Save it in a data structure, using cons or a record constructor.
These capabilities can help you assess the role of any species of value in any pro‐
gramming language; if you can do all these things with a value, that value is some‐
times said to be first class. “First‐class functions” is a phrase sometimes used to
characterize Scheme and other functional languages, but it’s not quite right: what’s
essential about Scheme is that it provides first‐class, nested functions.
To see what difference nesting makes, let’s start with a non‐nested example.
Because functions are first class, a µScheme function (defined with define or with
lambda) can be passed as an argument to another function. What can the function
that receives the argument do? All the standard things listed above, which it can
do with any first‐class value. And one more—the only interesting thing to do with a
function: apply it. As a simple example, function apply‑n‑times receives a func‐
tion f, an integer n, and an argument x, and it returns the result of applying f to x
n times:
121a. htranscript 95ai+≡ ◁ 120b 121b ▷
‑> (define apply‑n‑times (n f x)
(if (= 0 n)
x
(apply‑n‑times (‑ n 1) f (f x))))
‑> (apply‑n‑times 77 not #t)
#f
‑> (apply‑n‑times 78 not #t)
#t
Function add is defined by the outer lambda expression, which takes x as an argu‐
ment. It’s not so interesting. What’s interesting is inner lambda, which takes y as
an argument: every time it is evaluated, it creates a new function. The two lambdas
work together: when add receives argument m, it returns a function which, when it
receives argument n, returns m + n. (Therefore, add1 is a one‐argument function
which adds 1 to its argument.) Applying add to different integers can create arbitrar
ily many functions. Not only (add 1) but (add 2) and (add 100) can be functions.
Even an expression like (add (length xs)) creates a function.
New functions can be created because (lambda (y) (+ x y)) is nested inside the
outer lambda that defines add. Every time add is called, the inner lambda is evalu‐
ated, and in Scheme, every time a lambda expression is evaluated, a new function is
created. In languages like Impcore and C, which don’t have lambda, a new function
can be created only by writing it explicitly in the source code.
The expression (lambda (y) (+ x y)) can be evaluated to produce more than one
function. Such a function is represented by pairing the lambda expression with
an environment that says what x stands for. This pair forms the closure, which
we write in “banana brackets,” as in (|(lambda (y) (+ x y)), {x 7→ 1} |). The banana
brackets emphasize that a closure cannot be written directly using Scheme syntax;
the closure is a value that Scheme builds from a lambda expression. A closure is
the simplest way to implement first‐class, nested functions.
A closure is defined as a pair: code and an environment. In µScheme, the code
is a lambda expression, but what is the environment? In Impcore, an environment
maps a name to a value (or a function). But in Scheme, an environment maps a
name to a mutable location. You can think of a mutable location as a box containing
a value, which can change, or you can wear your C programmer’s hat and think of
it as a location in memory, which the environment can point to.
A Scheme environment maps each name to a mutable location because in a
world where there are closures, this is the easiest way to express the way assign‐
ment (set) works. In Impcore, set simply replaces an old environment with a new
one (and can even update the environment in place). But set can be implemented
in this way only because no environment is ever copied. In Scheme, by contrast,
an environment can be copied into any number of closures, and there is no easy
way for set to update them all. Instead, when a name is assigned to, set updates
the mutable location associated with that name.
In the following example, set updates a mutable location that is accessible only
from within a lambda. The inner lambda evaluates to a closure in which n points to
a mutable location. That closure is stored in global variable ten, and every time
ten is called, it updates n and returns the new value:
123a. htranscript 95ai+≡ ◁ 122 123b ▷
‑> (val counter‑from
(lambda (n)
§2.7
(lambda () (set n (+ n 1)))))
Language III:
‑> (val ten (counter‑from 10))
<function>
Firstclass
‑> (ten) functions, lambda,
11 and locations
‑> (ten)
123
12
‑> (ten)
13
When ten is defined, counter‑from is applied, and that application allocates a lo‐
cation ℓ to hold the value of n, which is 10. The environment in which the inner
lambda is evaluated therefore binds n to ℓ, and the inner lambda evaluates to the
closure (|(lambda () (set n (+ n 1))), {n 7→ ℓ, . . .} |), which is the value of ten.7
A mutable location can be shared among multiple closures, which can com‐
municate with each other by mutating it. For example, a mutable number n can be
shared by counter and reset functions, which are stored in a counter record:
123b. htranscript 95ai+≡ ◁ 123a 123c ▷
‑> (record counter [step reset])
‑> (val resettable‑counter‑from
(lambda (n) ; create a counter
(make‑counter (lambda () (set n (+ n 1)))
(lambda () (set n 0)))))
The two innermost lambda expressions refer to the same n, so that when a counter’s
reset function is called, its step function starts over at 0. And two different appli‐
cations of resettable‑counter‑from refer to different n’s, so that two independent
counters never interfere.
A counter is stepped or reset in two steps: extract a function from the counter
record, then apply it.
123c. htranscript 95ai+≡ ◁ 123b 123d ▷
‑> (val step (lambda (counter) ((counter‑step counter))))
‑> (val reset (lambda (counter) ((counter‑reset counter))))
The double applications are easy to overlook. A function is taken from the
counter record in an inner application, such as (counter‑step counter). That
function, which takes no parameters, is called in an outer application, such as
((counter‑step counter)).
In the following transcript, two counters, hundred and twenty, count indepen‐
dently and are reset independently.
123d. htranscript 95ai+≡ ◁ 123c 124a ▷
‑> (val hundred (resettable‑counter‑from 100))
‑> (val twenty (resettable‑counter‑from 20))
‑> (step hundred)
101
‑> (step hundred)
102
‑> (step twenty)
21
7
As suggested by the ellipsis, the environment contains other bindings, but the only bindings that
affect computation are the binding of n to ℓ and of + to a location containing the primitive addition
function.
Resetting hundred doesn’t affect twenty.
124a. htranscript 95ai+≡ ◁ 123d 124b ▷
‑> (reset hundred)
0
• It’s more flexible because the amount of shared mutable state isn’t limited by
the number of global variables.
• It’s better controlled because not every piece of code has access to it—the
only code that has access is code that has the relevant variables in a closure.
The mutable state that is stored in a closure is private to a function but also per
sistent across calls to that function. Private, persistent mutable state is sometimes
provided as a special language feature, usually called “own variables.” An own vari‐
able is local to a function, but its value is preserved across calls of the function.
The name comes from Algol 60, but own variables are also found in C, where they
are defined using the keyword static.
As another example of private, persistent mutable state, I define a simple
random‐number generator.8 It’s a higher‐order function that takes a next function
as parameter, and it keeps a private, mutable variable seed, which is initially 1:
124b. htranscript 95ai+≡ ◁ 124a 124c ▷
‑> (define mk‑rand (next)
(let ([seed 1])
(lambda () (set seed (next seed)))))
Parameter next can be any function that takes a number and returns another num‐
ber. To make a good random‐number generator requires a function that satisfies
some sophisticated statistical properties. A simple approximation uses the linear
congruential method (Knuth 1981, pp. 9–25) on numbers in the range 0 to 1023:
124c. htranscript 95ai+≡ ◁ 124b 124d ▷
‑> (define simple‑next (seed) (mod (+ (* seed 9) 5) 1024))
This generator is not, in any statistical sense, good. For one thing, after generating
only 1024 different numbers, it starts repeating. But it’s usable:
124d. htranscript 95ai+≡ ◁ 124c 125b ▷
‑> (val irand (mk‑rand simple‑next))
‑> (irand)
14
‑> (irand)
131
‑> (irand)
160
‑> (val repeatable‑irand (mk‑rand simple‑next))
‑> (repeatable‑irand)
14
‑> (irand)
421
8
Technically a generator of pseudorandom numbers.
Function irand has its own private copy of seed, which only it can access, and
which it updates at each call. And function repeatable‑irand, which might be
used to replay an execution for debugging, has its own private seed. So it repeats
the same sequence [1, 14, 131, 160, 421, . . .] no matter what happens with irand.
§2.7
2.7.2 Useful higherorder functions Language III:
Firstclass
The lambda expression does more than just encapsulate mutable state; lambda
functions, lambda,
helps express and support not just algorithms but also patterns of computation.
and locations
What a “pattern of computation” might be is best shown by example.
One minor example is the function mk‑rand: it can be viewed as a pattern that 125
says “if you tell me how to get from one number to the next, I can deliver an entire
sequence of numbers starting with 1.” This pattern of computation, while handy,
is not used often. More useful patterns can make new functions from old functions
or can express common ways of programming with lists, like “do something with
every element.” Such patterns are presented in the next few sections.
Composition
One of the simplest ways to make a new function is by composing two old ones.
Function o (pronounced “circle” or “compose”) returns the composition of two one‐
argument functions, often written f ◦ g . Composition is described by the algebraic
law (f ◦ g)(x) = f (g(x)), and like any function that makes new functions, it re‐
turns a lambda:
125a. hpredefined µScheme functions 96ai+≡ ◁ 106c 126c ▷
(define o (f g) (lambda (x) (f (g x)))) ; ((o f g) x) = (f (g x))
A lambda can be used to change the interface to a function, as in the example of hundred 123d
mod B
add (page 122):
reset 123c
125c. htranscript 95ai+≡ ◁ 125b 126a ▷ step 123c
‑> (val add (lambda (x) (lambda (y) (+ x y)))) twenty 123d
Function add does what + does, but add takes one argument at a time, whereas
+ takes both its arguments at once. For example, (add 1) is a function that adds
1 to its argument, while (+ 1) is an error. Similarly, (+ 1 2) is 3, while (add 1 2) is
an error. But add and + are really two forms of the same function; they differ only in
the way they take their arguments. The forms are named: add is the curried form,
and + is the uncurried form. The names honor the logician Haskell B. Curry.
Any function can be put into curried form; the curried form simply takes its
arguments one at a time. If it needs more than one argument, it takes the first
argument, then returns a lambda that expects the remaining arguments, also one
at a time. As another example, the curried form of the list3 function uses three
lambdas:
126a. htranscript 95ai+≡ ◁ 125c 126b ▷
‑> (val curried‑list3 (lambda (a) (lambda (b) (lambda (c) (list3 a b c)))))
126 Curried functions don’t mesh well with Scheme’s concrete syntax. Defining one
requires lots of lambdas, and applying one requires lots of parentheses.9 If currying
is so awkward, why bother with it? To get partial applications. A curried function is
partially applied when it is applied to only some of its arguments, and the resulting
function is saved to be applied later. If the function’s arguments are expected in the
right order, partial applications can be quite useful. For example, the curried form
of < can be partially applied to 0, and the resulting partial application takes any m
and says whether 0 < m:
126b. htranscript 95ai+≡ ◁ 126a 126d ▷
‑> (val <‑curried (lambda (n) (lambda (m) (< n m))))
‑> (val positive? (<‑curried 0))
‑> (positive? 0)
#f
‑> (positive? 8)
#t
‑> (positive? ‑3)
#f
Functions needn’t always be curried by hand. Any binary function can be con‐
verted between its uncurried and curried forms using the predefined functions
curry and uncurry:
126c. hpredefined µScheme functions 96ai+≡ ◁ 125a 129 ▷
(define curry (f) (lambda (x) (lambda (y) (f x y))))
(define uncurry (f) (lambda (x y) ((f x) y)))
(((curry f) x) y) = (f x y)
((uncurry f) x y) = ((f x) y)
As can be proved using the laws, the two functions are inverses; for example, if I
curry +, then uncurry the result, I get + back again:
126e. htranscript 95ai+≡ ◁ 126d 127a ▷
‑> (val also+ (uncurry (curry +)))
‑> (also+ 1 4)
5
9
More recently developed functional languages, like Standard ML, OCaml, and Haskell, use notations
and implementations that encourage currying and partial application, so much so that in OCaml and
Haskell, function definitions produce curried forms by default. For partial application in Scheme, see
the proposal by Egner (2002).
If currying makes your head hurt, don’t panic—I expect it. Both currying and
composition are easier to understand when they are used to make functions that
operate on values in lists.
These patterns, and more besides, are embodied in the higher‐order functions
filter, map, exists?, all?, and foldr, all of which are in µScheme’s initial ba‐
sis. (If you’ve heard of Google MapReduce, the Map is the same, and Reduce is a
parallel variant of foldr.)
Function filter takes a predicate p? and a list xs, and it returns a new list consist‐
ing of only those elements of xs that satisfy p?:
127a. htranscript 95ai+≡ ◁ 126e 127b ▷
‑> (define even? (x) (= (mod x 2) 0))
‑> (filter even? '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
As requested in Exercise 27, function filter can be used to define a concise version
of the remove‑multiples function from Section 2.3.4.
Function filter must receive a predicate as its first argument, but map works
with any unary function; (map f xs) returns the list of results formed by applying
function f to every element of list xs.
127b. htranscript 95ai+≡ ◁ 127a 128a ▷
‑> (map add1 '(3 4 5))
add1 122
(4 5 6) filter B 129
‑> (map ((curry +) 5) '(3 4 5)) list3 B 96
(8 9 10) map B 130
‑> (map (lambda (x) (* x x)) '(1 2 3 4 5 6 7 8 9 10)) mod B
(1 4 9 16 25 36 49 64 81 100) primes<= 102d
Functions filter and map build new lists from the elements of old ones—
a common pattern of computation. Another common pattern is linear search.
In µScheme, linear search is implemented by two functions, exists? and all?.
Each takes a predicate, and as you might expect, exists? tells whether there is an
element of the list satisfying the predicate; all? tells whether they all do.
128a. htranscript 95ai+≡ ◁ 127b 128b ▷
‑> (exists? even? '(1 2 3 4 5 6 7 8 9 10))
2 #t
‑> (all? even? '(1 2 3 4 5 6 7 8 9 10))
#f
Scheme, ‑> (all? even? (filter even? '(1 2 3 4 5 6 7 8 9 10)))
Sexpressions, and #t
firstclass functions ‑> (exists? even? (filter (o not even?) '(1 2 3 4 5 6 7 8 9 10)))
#f
128
When called on the empty list, an important “corner case,” exists? and all?
act like the mathematical ∃ and ∀.
128b. htranscript 95ai+≡ ◁ 128a 128c ▷
‑> (exists? even? '())
#f
‑> (all? even? '())
#t
More applications of foldr and foldl are suggested in Exercises 8, 29, and 30.
2.8.2 Visualizations of the standard list functions
Which list functions should be used when? Functions exists? and all? are not
hard to figure out, but map, filter, and foldr can be more mysterious. They can
be demystified a bit using pictures, as inspired by Harvey and Wright (1994).
A generic list xs can be depicted as a list of circles:
§2.8
xs = ··· Practice III:
If f is a function that turns one circle into one triangle, as in (f ) = 4, then Higherorder
(map f xs) turns a list of circles into a list of triangles. functions on lists
xs = ··· 129
y y y y y y
Most of the higher‐order list functions are easy to implement and easy to under‐
stand. Each is a recursive function with one base case (which consumes the empty all? B 130
list) and one induction step (which consumes a cons cell). All are part of the ini‐ even? 127a
exists? B 130
tial basis of µScheme. Except for app, they are described by the algebraic laws in foldl B 131
Figure 2.4 on the following page. foldr B 131
Function filter is structured in the same way as function remove‑multiples
from chunk 102b; the only difference is in the test. Filtering the empty list produces
the empty list. In the induction step, depending on whether the car satisfies p?,
filter may or may not cons.
129. hpredefined µScheme functions 96ai+≡ ◁ 126c 130a ▷
(define filter (p? xs)
(if (null? xs)
'()
(if (p? (car xs))
(cons (car xs) (filter p? (cdr xs)))
(filter p? (cdr xs)))))
(filter p? '()) = '()
(filter p? (cons y ys)) = (cons y (filter p? ys)), when (p? y)
(filter p? (cons y ys)) = (filter p? ys), when (not (p? y))
2 (map f '())
(map f (cons y ys))
= '()
= (cons (f y) (map f ys))
Scheme, (exists? p? '()) = #f
Sexpressions, and (exists? p? (cons y ys)) = #t, when (p? y)
firstclass functions (exists? p? (cons y ys)) = (exists? p? ys), when (not (p? y))
(all? p? '()) = #t
130
(all? p? (cons y ys)) = (all? p? ys), when (p? y)
(all? p? (cons y ys)) = #f, when (not (p? y))
(foldr combine zero '()) = zero
(foldr combine zero (cons y ys)) = (combine y (foldr combine zero ys))
Function map is even simpler. There is no conditional test; the induction step
just applies f to the car, then conses.
130a. hpredefined µScheme functions 96ai+≡ ◁ 129 130b ▷
(define map (f xs)
(if (null? xs)
'()
(cons (f (car xs)) (map f (cdr xs)))))
Function app is like map, except its argument is applied only for side effect.
Function app is typically used with printu. Because app is executed for side ef‐
fects, its behavior cannot be expressed using simple algebraic laws.
130b. hpredefined µScheme functions 96ai+≡ ◁ 130a 130c ▷
(define app (f xs)
(if (null? xs)
#f
(begin (f (car xs)) (app f (cdr xs)))))
Each of the preceding functions processes every element of its list argument.
Functions exists? and all? don’t necessarily do so. Function exists? stops
the moment it finds a satisfying element; all? stops the moment it finds a non‐
satisfying element.
130c. hpredefined µScheme functions 96ai+≡ ◁ 130b 131b ▷
(define exists? (p? xs)
(if (null? xs)
#f
(if (p? (car xs))
#t
(exists? p? (cdr xs)))))
(define all? (p? xs)
(if (null? xs)
#t
(if (p? (car xs))
(all? p? (cdr xs))
#f)))
Function all? could also be defined using De Morgan’s law, which says that
¬∀x.P (x) = ∃x.¬P (x). Negating both sides gives this definition:
131a. htranscript 95ai+≡ ◁ 128c 131c ▷
‑> (define alt‑all? (p? xs) (not (exists? (o not p?) xs)))
‑> (alt‑all? even? '(1 2 3 4 5 6 7 8 9 10))
§2.9
#f
Practice IV:
‑> (alt‑all? even? '())
#t
Higherorder
‑> (alt‑all? even? (filter even? '(1 2 3 4 5 6 7 8 9 10))) functions for
#t polymorphism
Finally, foldr and foldl, although simple, are not necessarily easy to under‐ 131
stand. Study their algebraic laws, and remember that (car xs) is always a first
argument to combine, and zero is always a second argument.
131b. hpredefined µScheme functions 96ai+≡ ◁ 130c
(define foldr (combine zero xs)
(if (null? xs)
zero
(combine (car xs) (foldr combine zero (cdr xs)))))
(define foldl (combine zero xs)
(if (null? xs)
zero
(foldl combine (combine (car xs) zero) (cdr xs))))
A function like filter doesn’t need to know what sort of value predicate p? is ex‐
pecting. For example, filter can be used with function even? to select elements of
a list of numbers; or it can be used with function (o even? alist‑pair‑attribute)
to select elements of an association list that contains symbol‐number pairs; or
it can be used with infinitely many other combinations of predicates and lists.
The ability to be used with arguments of many different types makes filter poly
morphic (see sidebar on the next page). Functions exists?, all?, map, foldl, and
foldr are also polymorphic. By contrast, a function that works with only one type
of argument, like <, is monomorphic. Polymorphic functions are especially easy to
reuse. In this section, polymorphism is demonstrated in examples that implement
set operations and sorting.
As shown in chunks 105a and 105c above, set operations can be implemented
using recursive functions. But they can be made more compact and (eventually)
easier to understand by using the higher‐order functions exists?, curry, and
foldl. cons P S313d
curry B 126
131c. htranscript 95ai+≡ ◁ 131a 133a ▷
equal? B 104
‑> (val emptyset '()) even? 127a
‑> (define member? (x s) (exists? ((curry equal?) x) s)) filter B 129
‑> (define add‑element (x s) (if (member? x s) s (cons x s)))
‑> (define union (s1 s2) (foldl add‑element s1 s2))
‑> (define set‑of‑list (xs) (foldl add‑element '() xs))
‑> (set‑of‑list '(a b c x y a))
(y x c b a)
‑> (union '(1 2 3 4) '(2 4 6 8))
(8 6 1 2 3 4)
These set functions work on sets of atoms, and because member? calls equal?,
they also work on sets of lists of atoms. But they won’t work on other kinds of sets,
like sets of sets or sets of association lists; equal? is too pessimistic.
Three kinds of polymorphism
When programmers can say “code reuse” with a fancy Greek name, they sound
2 smart. What’s less smart is that at least three different programming techniques
are all called “polymorphism.”
• The sort of polymorphism we find in the standard list functions is called
Scheme,
parametric polymorphism.a In parametric polymorphism, the polymor‐
Sexpressions, and
phic code always executes the same algorithm in the same way, regard‐
firstclass functions
less of the types of the arguments. Parametric polymorphism is the sim‐
132 plest kind of polymorphism, and although it is most useful when com‐
bined with higher‐order functions, it can be implemented without spe‐
cial mechanisms at run time. Parametric polymorphism is found in some
form in every functional language.
• Finally, in some languages, a single symbol can stand for unrelated func‐
tions. For example, in Python, the symbol + is used not only to add num‐
bers but also to concatenate strings. A number is not a kind of string,
and a string is not a kind of number, and the algorithms are unrelated.
Nonetheless, + works on more than one type of argument, so it is consid‐
ered polymorphic. This kind of polymorphism is called ad hoc polymor
phism, but Anglo‐Saxon people tend to call it overloading. It is described,
in passing, in Chapter 9.
To address the issue, let’s focus on sets of association lists. Two association lists
are considered equal if they each have the same keys and attributes, regardless of
how the keyattribute pairs are ordered. In other words, although an association list
is represented in the world of code as a sequence of key‐attribute pairs, what it stands
for in the world of ideas is a set of key‐value pairs. Therefore, two association lists
are equal if and only if each contains all the key‐value pairs found in the other:
133a. htranscript 95ai+≡ ◁ 131c 134c ▷
‑> (define sub‑alist? (al1 al2)
; all of al1's pairs are found in al2
(all? (lambda (pair)
(equal? (alist‑pair‑attribute pair)
(find (alist‑pair‑key pair) al2))) §2.9
al1)) Practice IV:
‑> (define =alist? (al1 al2) Higherorder
(and (sub‑alist? al1 al2) (sub‑alist? al2 al1))) functions for
‑> (=alist? '() '()) polymorphism
#t
‑> (=alist? '((E coli) (I Magnin) (U Thant)) 133
'((E coli) (I Ching) (U Thant)))
#f
‑> (=alist? '((U Thant) (I Ching) (E coli))
'((E coli) (I Ching) (U Thant)))
#t
The style is identified by the location in which the predicate is stored: B 106
alist‑pair‑key
B 106
• In the simplest style, a new parameter, the equality predicate my‑equal?,
all? B 130
is added to every function. The modified functions look like this: and B
133b. hpolymorphicset transcript 133bi≡ (S320c) 134a ▷ cons P S313d
‑> (define member? (x s my‑equal?) curry B 126
equal? B 104
(exists? ((curry my‑equal?) x) s))
exists? B 130
member? find B 106
‑> (define add‑element (x s my‑equal?)
(if (member? x s my‑equal?) s (cons x s)))
add‑element
The best feature of this style is that a predicate is supplied only when con‐
structing an empty set.
• The second style imposes embarrassing run‐time costs. Each set must con‐
tain the equality predicate, which adds extra memory to each set, and which
requires a level of indirection to gain access either to the equality predicate
or to the elements. The second style also imposes a lesser but still nontrivial
burden on client code, which has to propagate the equality predicate to each
point where an empty set might be created.
In the third style, these issues are addressed by storing the equality predi‐
cate in the operations. Each operation is represented by a closure, and the
equality function is placed the environment of that closure. In this style, no
extra memory is added to any set, a set’s elements are accessed without in‐
direction, and the equality predicate is cheaper to fetch from a closure than
it would be from a set. And client code has to supply each equality predicate
only once, to create the specialized operations that work with that predicate.
In a third‐style implementation of sets, the equality predicate is placed into
closures by function set‑ops‑with, which returns a record of set operations.
Each operation is specialized to use the given equality.
134c. htranscript 95ai+≡ ◁ 133a 135a ▷
‑> (record set‑ops [member? add‑element])
‑> (define set‑ops‑with (my‑equal?)
(make‑set‑ops
(lambda (x s) (exists? ((curry my‑equal?) x) s)) ; member?
(lambda (x s) ; add‑element
(if (exists? ((curry my‑equal?) x) s) s (cons x s)))))
These operations can be used without mentioning the equality predicate. §2.9
Practice IV:
135b. htranscript 95ai+≡ ◁ 135a 135c ▷
Higherorder
‑> (val emptyset '())
‑> (val s (al‑add‑element '((U Thant) (I Ching) (E coli)) emptyset))
functions for
(((U Thant) (I Ching) (E coli))) polymorphism
‑> (val s (al‑add‑element '((Hello Dolly) (Goodnight Irene)) s))
135
(((Hello Dolly) (Goodnight Irene)) ((U Thant) (I Ching) (E coli)))
‑> (val s (al‑add‑element '((E coli) (I Ching) (U Thant)) s))
(((Hello Dolly) (Goodnight Irene)) ((U Thant) (I Ching) (E coli)))
‑> (al‑member? '((Goodnight Irene) (Hello Dolly)) s)
#t
Another widely used, polymorphic algorithm is sorting. Good sorts are tuned for
performance, and when code is tuned for performance, it should be reused. And a
sort function can be reused in the same way as the set functions: just as sets require
an equality function that operates on set elements, sorting requires a comparison
function that operates on list elements. So like the set operations, a sort function
should take the comparison function as a parameter. For example, the polymor‐
phic, higher‐order function mk‑insertion‑sort takes a comparison function lt?,
and returns a function that sorts a list of elements into nondecreasing order (ac‐
cording to function lt?). The algorithm is from chunk 101a.
135c. htranscript 95ai+≡ ◁ 135b 135d ▷
‑> (define mk‑insertion‑sort (lt?)
(letrec ([insert (lambda (x xs)
(if (null? xs)
(list1 x)
(if (lt? x (car xs))
(cons x xs)
(cons (car xs) (insert x (cdr xs))))))]
[sort (lambda (xs)
(if (null? xs) =alist? 133a
'() car P 162a
cdr P 162a
(insert (car xs) (sort (cdr xs)))))])
cons P S313d
sort)) curry B 126
This definition includes our first example of letrec, which is like let, but which emptyset 105a
exists? B 130
makes each bound name visible to all the others. Internal functions sort and
list1 B 96
insert are both recursive, and because both are defined in same letrec, either null? P 162a
one can also call the other.
Function mk‑insertion‑sort makes sorting flexible and easy to reuse. For ex‐
ample, when used with appropriate comparison functions, it can make both in‐
creasing and decreasing sorts.
135d. htranscript 95ai+≡ ◁ 135c 137a ▷
‑> (val sort‑increasing (mk‑insertion‑sort <))
‑> (val sort‑decreasing (mk‑insertion‑sort >))
‑> (sort‑increasing '(6 9 1 7 4 3 8 5 2 10))
(1 2 3 4 5 6 7 8 9 10)
‑> (sort‑decreasing '(6 9 1 7 4 3 8 5 2 10))
(10 9 8 7 6 5 4 3 2 1)
Function mk‑insertion‑sort implements the same pattern of computation as
mk‑rand, only more useful: “if you tell me when one element should come before
another, I can sort a list of elements.” It can equally well sort lists by length, sort
pairs lexicographically, and whatever else you need. That’s the power of higher‐
2 order, polymorphic functions.
When it succeeds or fails, find‑c exits. But continuations can also more sophis‐
ticated control flow, like the ability to backtrack on failure. In direct‐style imper‐
ative code, backtracking is hard to get right, but using continuations and purely
functional data structures, it’s easy. And a backtracking problem can demonstrate
the virtues of leaving the answer type unknown.
One classic problem that can be solved with backtracking is Boolean satisfac
tion. The problem is to find an assignment to a collection of variables such that a
Boolean formula is satisfied, that is, a satisfying assignment. This problem has many
11
Properly speaking, continuation‐passing style is a representation of programs in which all func‐
tions, even primitive functions, end by transferring control to a continuation (Appel 1992).
applications, often in verifying the correctness of a hardware or software system.
For example, on a railroad, if the signals are obeyed, no two trains should ever be
on the same track at the same time, and this property can be checked by solving a
very large Boolean‐satisfaction problem.
The satisfaction problem can be simplified by considering only only formu‐
las in conjunctive normal form. (General Boolean formulas are the subject of Exer‐
§2.10
cise 41.) A conjunctive normal form is a conjunction of disjunctions of literals:
Practice V:
CNF ::= D1 ∧ D2 ∧ · · · ∧ Dn conjunction, Continuation
passing style
D ::= l1 ∨ l2 ∨ · · · ∨ lm disjunction,
l ::= x | ¬x literal. 139
The x is a metavariable that may stand for any Boolean variable.
A CNF formula is satisfied if all of its disjunctions Di are satisfied, and a dis‐
junction is satisfied if any of is literals li is satisfied. A literal x is satisfied if x is
true; a literal ¬x is satisfied if x is false. A formula be satisfied by more than one
assignment; for example, the formula x ∨ y ∨ z is satisfied by 7 of the 8 possible as‐
signments to x, y, z . Satisfying assignments can be found by a backtracking search.
Because the satisfaction problem is NP‐hard, a search might take exponential time.
The search algorithm presented below incrementally improves an incomplete
assignment. An incomplete assignment associates values with some variables—
all, none, or some number in between. An incomplete assignment is represented
by an association list in which each key is the name of a variable and each value is
#t or #f. A variable that doesn’t appear in the list is unassigned.
Given disjunction Di and an incomplete assignment cur, the search algorithm
tries to extend cur by adding variables in such a way that Di is satisfied. If that
works, the algorithm continues by trying to extend the assignment to satisfy Di+1 .
But if it can’t satisfy Di , the algorithm doesn’t give up; instead it backtracks to Di−1 .
Maybe Di−1 can be satisfied in a different way, such that it becomes possible to
satisfy Di . As an example, suppose D1 = x ∨ y ∨ z , and the search algorithm finds
the assignment {x 7→ #t, y 7→ #f, z 7→ #f}, which satisfies it. If D2 = ¬x ∨ y ∨ z ,
the search has to go back and find a different assignment to satisfy D1 . The search
can be viewed as a process of going back and forth over the Di ’s, tracing a path
through this graph:
cadr B 96
The graph is composed of solvers that look like this: cdr P 162a
curry B 126
start succeed filter B 129
followers 137d
solver
null? P 162a
fail resume
Thanks to the graph structure, a solver can just tackle an individual Di without
keeping track of what it’s supposed to do afterward. Its future obligations are stored
in two continuations, succeed and fail, which it receives as parameters. The third
continuation in the diagram, written as resume, is the failure continuation for the
next solver: if the next solver can’t succeed, it calls resume to ask the current solver
to try something else.
The graph structure and its continuations are used to solve formulas in all three
forms: CNF , D , and l. Each form is solved by its own function. For example,
a CNF formula D1 ∧ · · · ∧ Dn is solved by a function that receives the formula,
an incomplete assignment cur, and continuations succeed and fail. When the
solver is called, as represented above by the arrow labeled “start,” it acts as follows:
• If cur cannot possibly satisfy the first disjunction D1 , call fail with no pa‐
2 rameters.
140 To make this story precise requires precise specifications of the solver functions
and of a representation of formulas.
The representation of CNF , the set of formulas in conjunctive normal form,
is described by the following equations, which resemble the recursion equations
in Section 2.5.8:
In conjunctive normal form, the symbols ∧ and ∨ are implicit. The ¬ symbol is
represented explicitly using the Scheme symbol not.
In the code, the forms are referred to by names ds, lits, and lit. Assignments
and continuations are also referred to by conventional names:
Each function calls succeed on success and fail on failure. The specifications can
be made precise using algebraic laws.
The laws for any given solver function depend on what species of formula the
function solves. Each species is solved by a different algorithm, which depends on
how a formula in the species is satisfied. For example, a CNF formula is satisfied
if all its disjunctions are satisfied. There are two cases:
• If there are no disjunctions, then cur trivially satisfies them all. And when
cur is passed to succeed, nothing else can be done with an empty list of dis‐
junctions, so the resumption continuation is fail.
• If there is a disjunction, it should be solved by calling the solver for disjunc‐
tions: find‑D‑true‑assignment. If that call fails, nothing more can be done,
so its failure continuation should be fail. But if that call succeeds, the search
needs to continue by solving the remaining disjunctions. So the CNF solver
creates a new continuation to be passed as the success continuation. The
new continuation calls find‑cnf‑true‑assignment recursively, and if the
§2.10
recursive call fails, it backtracks by transferring control to resume.
Practice V:
Each case is described by a law: Continuation
passing style
(find‑cnf‑true‑assignment '() cur fail succeed)) = (succeed cur fail)
141
(find‑cnf‑true‑assignment (cons d ds) cur fail succeed)) =
(find‑D‑true‑assignment d cur fail
(lambda (cur' resume) (find‑cnf‑true‑assignment ds cur' resume succeed)))
You might be concerned that if the first literal is solved successfully, the later lit‐
erals will never be examined, and some possible solutions might be missed. But
those later literals lits are examined by the failure continuation that is passed to
find‑lit‑true‑assignment, which is eventually passed to succeed as a resump‐
tion continuation. If anything goes wrong downstream, that resumption continu‐
ation is eventually invoked, and it calls (find‑D‑true‑assignment lits · · · ).
The last of the three solver functions solves a literal. It needs to know what
variable appears in a literal and what value would satisfy the literal.
141a. hutility functions for solving literals 141ai≡ 141b ▷
‑> (define variable‑of (lit)
(if (symbol? lit)
lit
(cadr lit))) cadr B 96
‑> (define satisfying‑value (lit) find‑c 136
(symbol? lit)) ; #t satisfies 'x; #f satisfies '(not x) symbol? P 162a
Because an ordinary literal is a symbol and a negated literal is a list like (not x),
the value that satisfies a literal lit is always equal to (symbol? lit).
A literal is satisfied if and only if the current assignment binds the literal’s vari‐
able to a satisfying value. The test uses find‑c with a Boolean answer type.
141b. hutility functions for solving literals 141ai+≡ ◁ 141a 142a ▷
‑> (define satisfies? (alist lit)
(find‑c (variable‑of lit) alist
(lambda (b) (= b (satisfying‑value lit)))
(lambda () #f)))
The last solver function, find‑lit‑true‑assignment, succeeds either when
the current assignment already satisfies its literal, or when it can be made to sat‐
isfy the literal by adding a binding. There is at most one way to succeed, so the
resumption continuation passed to succeed is always fail.
142 To determine that cur does not bind x, the solver function uses find‑c with yet
another pair of continuations:
142a. hutility functions for solving literals 141ai+≡ ◁ 141b
‑> (define binds? (alist lit)
(find‑c (variable‑of lit) alist (lambda (_) #t) (lambda () #f)))
All three solver functions are coded in Figure 2.5 on the facing page. And they
can be used with different continuations that produce answers of different types.
For example, the CNF solver can be used to tell whether a formula is satisfiable, to
produce one solution, or to produce all solutions. To see if a formula has a solution,
use find‑cnf‑true‑assignment with a success continuation that returns #t and a
failure continuation that returns false:
142b. htranscript 95ai+≡ ◁ 138 142c ▷
‑> (define satisfiable? (formula)
(find‑cnf‑true‑assignment formula '()
(lambda () #f)
(lambda (cur resume) #t)))
has a solution.
142c. htranscript 95ai+≡ ◁ 142b 142d ▷
‑> (val sample‑formula '((x y z) ((not x) (not y) (not z)) (x y (not z))))
‑> (satisfiable? sample‑formula)
#t
This formula is satisfied if x is true and y is false; the value of z doesn’t matter.
To find all solutions, use a success continuation that adds the current solution
to the set returned by resume. (To avoid adding duplicate solutions, cur is added
using al‑add‑element, not cons.) The failure continuation returns the empty set.
142e. htranscript 95ai+≡ ◁ 142d 143b ▷
‑> (define all‑solutions (formula)
(find‑cnf‑true‑assignment
formula
'()
(lambda () emptyset)
(lambda (cur resume) (al‑add‑element cur (resume)))))
143a. hsolver functions for CNF formulas 143ai≡
‑> (define find‑lit‑true‑assignment (lit cur fail succeed)
(if (satisfies? cur lit)
(succeed cur fail)
(if (binds? cur lit)
(fail)
(let ([new (bind (variable‑of lit) (satisfying‑value lit) cur)]) §2.10
(succeed new fail))))) Practice V:
‑> (define find‑D‑true‑assignment (literals cur fail succeed)
Continuation
(if (null? literals)
passing style
(fail)
(find‑lit‑true‑assignment (car literals) cur 143
(lambda () (find‑D‑true‑assignment
(cdr literals) cur fail succeed))
succeed)))
‑> (define find‑cnf‑true‑assignment (disjunctions cur fail succeed)
(if (null? disjunctions)
(succeed cur fail)
(find‑D‑true‑assignment (car disjunctions) cur fail
(lambda (cur' resume)
(find‑cnf‑true‑assignment
(cdr disjunctions) cur' resume succeed)))))
Both the solver and find‑c demonstrate that a function written in continuation‐
passing style can often be used in different ways, simply by passing it different
continuations with different answer types.
2.11 OPERATıONAL ſEMANTıCſ
A µScheme value is a symbol, number, Boolean, empty list, cons cell, clo‐
sure, or primitive function. These values are written ſYMBOL(s), NUMBER(n),
BOOLV(b), NıL, PAıRhℓ1 , ℓ2 i, (|LAMBDA(hx1 , . . . , xn i, e), ρ |), and PRıMıTıVE(p).
The corresponding data definition is shown with the interpreter (Section 2.12).
The most interesting parts of the semantics are the rules for lambda, let, and func‐
tion application. The key ideas involve mutable locations:
• Function application also allocates fresh locations, which hold the values of
actual parameters. These locations are then bound to the names of the for‐
mal parameters of the function being applied.
Variables
The single environment makes it easy to look up the value of a variable. Lookup
requires two steps: ρ(x) to find the location in which x is stored, and σ(ρ(x)) to
fetch its value. In a compiled system, these two steps are implemented at different
times. At compile time, the compiler decides in what location to keep x. At run
time, a machine instruction fetches a value from that location.
Looking up a variable doesn’t change the store σ .
x1 , . . . , xn all distinct
ℓ1 , . . . , ℓn ∈/ dom σn (and all distinct)
σ0 = σ
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ, σn−1 i ⇓ hvn , σn i
he, ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
(LET)
hLET(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
Locations ℓ1 , . . . , ℓn are not only fresh but also mutually distinct.
By contrast, a LETſTAR expression binds the result of each evaluation into the
environment immediately. The action is revealed by the subscripts on ρ.
ρ0 = ρ σ0 = σ
he1 , ρ0 , σ0 i ⇓ hv1 , σ0′ i ℓ1 ∈
/ dom σ0′ ρ1 = ρ0 {x1 7→ ℓ1 } σ1 = σ0′ {ℓ1 7→ v1 }
..
′
.
hen , ρn−1 , σn−1 i ⇓ hvn , σn−1 i
′ ′
ℓn ∈
/ dom σn−1 ρn = ρn−1 {xn 7→ ℓn } σn = σn−1 {ℓn 7→ vn }
′
he, ρn , σn i ⇓ hv, σ i
hLETſTAR(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
(LETſTAR)
Finally, LETREC binds the locations into the environment before evaluating the
expressions, so that references to the new names are valid in every right‐hand side,
and the names stand for the new locations, not for any old ones.
Functional abstraction wraps the current environment, along with a lambda ex‐
§2.11
pression, in a closure. LAMBDA copies the current environment. It is because en‐
Operational
vironments can be copied that they map names to locations, not values; otherwise
semantics
different closures could not share a mutable location. (One example of such shar‐
ing is the “resettable counter” in Section 2.7.1.) 147
x1 , . . . , xn all distinct
hLAMBDA(hx1 , . . . , xn i, e), ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, e), ρ |), σi
(MĸCLOſURE)
When a closure (|LAMBDA(hx1 , . . . , xn i, ec ), ρc |) is applied, the body of the
function, ec , is evaluated using the environment in the closure, ρc , which is ex‐
tended by binding the formal parameters to fresh locations ℓ1 , . . . , ℓn . These loca‐
tions are initialized with the values of the actual parameters; afterward, the body
can change them.
ℓ1 , . . . , ℓ n ∈
/ dom σn (and all distinct)
he, ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, ec ), ρc |), σ0 i
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ, σn−1 i ⇓ hvn , σn i
hec , ρc {x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
hAPPLY(e, e1 , . . . , en ), ρ, σi ⇓ hv, σ ′ i
(APPLYCLOſURE)
The APPLYCLOſURE rule closely resembles the APPLYUſER rule used in Impcore.
The crucial differences are as follows:
• In µScheme, the parameters are added not to the empty environment but to
the environment ρc stored in the closure. And like let‐bound names, the
formal parameters are bound to fresh locations, not directly to values.
Function application takes the environment in a closure and extends it with bind‐
ings for formal parameters:
ρc {x1 7→ v1 , . . . , xn 7→ vn }.
The idea is that formal parameters hide outer variables of the same name. To ex‐ §2.11
press that idea formally, formal parameters can be placed in an environment of Operational
their own: ρf = {x1 7→ v1 , . . . , xn 7→ vn }. The two environments can then be semantics
combined using a + symbol: ρc + ρf . The combined environment, in which the
body of the function is evaluated, obeys the following laws: 149
The rules for evaluating the other expressions of µScheme are very similar to the
corresponding rules of Impcore.
As in Impcore, a literal value evaluates to itself without changing the store.
(LıTERAL)
hLıTERAL(v), ρ, σi ⇓ hv, σi
Conditionals, loops, and sequences are evaluated as in Impcore, except that
falsehood is represented by BOOLV(#f), not 0.
he, ρ, σ0 i ⇓ hPRıMıTıVE(=), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
v1 ≡ v2
(APPLYEQTRUE)
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hBOOLV(#t), σ3 i
he, ρ, σ0 i ⇓ hPRıMıTıVE(=), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
v1 6≡ v2 (i.e., no proof of v1 ≡ v2 )
(APPLYEQFALſE)
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hBOOLV(#f), σ3 i
Printing As in Impcore, the operational semantics takes no formal notice of print‐
ing, so the semantics of println are those of the identity function.
he, ρ, σ0 i ⇓ hPRıMıTıVE(println), σ1 i
he1 , ρ, σ1 i ⇓ hv, σ2 i
(APPLYPRıNTLN)
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hv, σ2 i while printing v
Primitive println has the same semantics as print, but printu has a more restric‐
tive semantics: it is defined only when the code point is a suitable number.
he, ρ, σ0 i ⇓ hPRıMıTıVE(printu), σ1 i
he1 , ρ, σ1 i ⇓ hNUMBER(n), σ2 i
0 ≤ n < 216
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hNUMBER(n), σ2 i while printing the UTF‐8 coding of n
(APPLYPRıNTU)
List operations The primitive CONſ builds a new cons cell. The cons cell is a pair
of locations that hold the values of the car and cdr.
he, ρ, σ0 i ⇓ hPRıMıTıVE(cons), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
ℓ1 ∈ / dom σ3 ℓ2 ∈
/ dom σ3 ℓ1 6= ℓ2
(CONſ) §2.11
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hPAıRhℓ1 , ℓ2 i, σ3 {ℓ1 7→ v1 , ℓ2 7→ v2 }i
Operational
Primitives car and cdr observe cons cells. semantics
151
he, ρ, σ0 i ⇓ hPRıMıTıVE(car), σ1 i
he1 , ρ, σ1 i ⇓ hPAıRhℓ1 , ℓ2 i, σ2 i
(CAR)
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hσ2 (ℓ1 ), σ2 i
he, ρ, σ0 i ⇓ hPRıMıTıVE(cdr), σ1 i
he1 , ρ, σ1 i ⇓ hPAıRhℓ1 , ℓ2 i, σ2 i
(CDR)
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hσ2 (ℓ2 ), σ2 i
If the result of evaluating e1 is not a PAıR, rules CAR and CDR do not apply, and the
abstract machine gets stuck. In such a case, my interpreter issues a run‐time error
message.
A definition typically adds a new binding to the environment and changes the store.
Its evaluation judgment is hd, ρ, σi → hρ′ , σ ′ i, which says that evaluating defini‐
tion d in environment ρ with store σ produces a new environment ρ′ and a new
store σ ′ .
Global variables
x ∈ dom ρ
he, ρ, σi ⇓ hv, σ ′ i
(DEFıNEOLDGLOBAL)
hVAL(x, e), ρ, σi → hρ, σ ′ {ρ(x) 7→ v}i
If x is not already bound, VAL allocates a fresh location ℓ, extends the environment
to bind x to ℓ, evaluates the expression in the new environment, and stores the
result in ℓ:
x 6∈ dom ρ ℓ 6∈ dom σ
he, ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}i ⇓ hv, σ ′ i .
(DEFıNENEWGLOBAL)
hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ {ℓ 7→ v}i
The “unspecified” value stored in ℓ effectively means that an adversary gets to look
at your code and choose the least convenient value. Code that depends on an un‐
specified value invites disaster, or at least an unchecked run‐time error.
Why does VAL add the binding to ρ before a value is available to store in ℓ? To en‐
able a recursive function to refer to itself. To see the need, consider what would
happen in the following definition of factorial if the binding were not added until
after the lambda expression was evaluated:
(val fact
2 (lambda (n)
(if (= n 0)
1
Scheme, (* n (fact (‑ n 1))))))
Sexpressions, and
firstclass functions If the binding isn’t added before the closure is created, then when the body of the
lambda expression is evaluated, it won’t be able to call fact—because the environ‐
152 ment saved in the closure won’t contain a binding for fact. That’s why the seman‐
tics for VAL in µScheme is different from the semantics for VAL in Impcore.
Toplevel functions
Toplevel expressions
A top‐level expression is syntactic sugar for a binding to the global variable it.
The representation of values is generated from the data definition shown here.
The empty list is called NIL, which is its name in Common Lisp. A function is repre‐
sented by a CLOSURE, which is a Lambda with an environment; the Lambda structure
contains the function’s formal‐parameter names and body.
152a. hvalue.t 152ai≡
Lambda = (Namelist formals, Exp body)
Value = SYM (Name)
| NUM (int32_t)
| BOOLV (bool)
| NIL
| PAIR (Value *car, Value *cdr)
| CLOSURE (Lambda lambda, Env env)
| PRIMITIVE (int tag, Primitive *function)
2.12.2 Interfaces
In the operational semantics, the store σ models the memory of the abstract ma‐ type Exp A
type Name 43a
chine. In the implementation, the store is represented by the memory of the real type Namelist
machine; a location is represented by a C pointer of type Value *. An environment 43a
Env maps names to pointers; find(x, ρ) returns ρ(x) whenever x ∈ dom ρ; when type Value A
type Valuelist
x∈/ dom ρ, it returns NULL.
S309c
153a. htype definitions for µScheme 145bi+≡ (S318a) ◁ 152b
typedef struct Env *Env;
Allocation
The fresh locations created by bindalloc and bindalloclist come from allocate.
Calling allocate(v) finds a location ℓ ∈
/ dom σ , stores v in ℓ (thereby updating σ ),
and returns ℓ.
154a. hfunction prototypes for µScheme 153bi+≡ (S318a) ◁ 153c 154b ▷
Value *allocate(Value v);
Values
Values are represented as specified by the data definition in chunk 152a. For con‐
venience, values #t and #f are always available in C variables truev and falsev.
154b. hfunction prototypes for µScheme 153bi+≡ (S318a) ◁ 154a 154c ▷
extern Value truev, falsev;
Values can be tested for truth or falsehood by function istrue; any value dif‐
ferent from #f is regarded as true.
154c. hfunction prototypes for µScheme 153bi+≡ (S318a) ◁ 154b 154d ▷
bool istrue(Value v);
When an unspecified value is called for by the semantics, one can be obtained
by calling function unspecified.
154d. hfunction prototypes for µScheme 153bi+≡ (S318a) ◁ 154c 155a ▷
Value unspecified(void);
If you get the µScheme interpreter to crash, your µScheme code is probably looking
at a value returned by unspecified. That’s an unchecked run‐time error.
Evaluation
For example, eval(e, ρ), when evaluated with store σ , finds a v and a σ ′ such that
he, ρ, σi ⇓ hv, σ ′ i, updates the store to be σ ′ , and returns v .
155a. hfunction prototypes for µScheme 153bi+≡ (S318a) ◁ 154d
Value eval (Exp e, Env rho);
Env evaldef(Def d, Env rho, Echo echo);
Printing
Just like the Impcore interpreter, the µScheme interpreter uses functions print
and fprint, but the µScheme interpreter knows how to print more kinds of things.
The alternatives are shown in Table 2.7. Most of these specifications are used only
to debug the interpreter.
As in Impcore, the evaluator starts with switch, which chooses how to evaluate e
based on its syntactic form: type Def A
type Echo S293f
155b. heval.c 155bi≡ 157c ▷
type Env 153a
Value eval(Exp e, Env env) { type Exp A
switch (e‑>alt) { type Value A
case LITERAL: hevaluate e‑>literal and return the result 156ai
case VAR: hevaluate e‑>var and return the result 156bi
case SET: hevaluate e‑>set and return the result 156ci
case IFX: hevaluate e‑>ifx and return the result 159bi
case WHILEX: hevaluate e‑>whilex and return the result 159ci
case BEGIN: hevaluate e‑>begin and return the result 159di
case APPLY: hevaluate e‑>apply and return the result 156ei
case LETX: hevaluate e‑>letx and return the result 157di
case LAMBDAX: hevaluate e‑>lambdax and return the result 156di
}
assert(0);
}
Literals
2 return e‑>literal;
switch (f.alt) {
case PRIMITIVE:
happly f.primitive to vs and return the result 157ai
case CLOSURE:
happly f.closure to vs and return the result 157bi
default:
runerror("%e evaluates to non‑function %v in %e",
e‑>apply.fn, f, e);
}
}
Because a primitive is represented by a pair containing a function pointer and
a tag, its application is simpler than in Impcore. Function f.primitive.function
gets the tag, the arguments vs, and the abstract syntax e. (The syntax is used in
error messages.)
157a. happly f.primitive to vs and return the result 157ai≡ (156e)
return f.primitive.function(e, f.primitive.tag, vs);
A closure is applied by extending its stored environment (ρc in the operational
semantics) with the bindings for the formal variables, then evaluating the body in §2.12
that environment. The interpreter
ℓ1 , . . . , ℓ n ∈
/ dom σn (and all distinct) 157
he, ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, ec ), ρc |), σ0 i
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ, σn−1 i ⇓ hvn , σn i
hec , ρc {x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
hAPPLY(e, e1 , . . . , en ), ρ, σi ⇓ hv, σ ′ i
(APPLYCLOſURE)
x1 , . . . , xn all distinct
2 ℓ1 , . . . , ℓn ∈/ dom σn (and all distinct)
σ0 = σ
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
Scheme, ..
Sexpressions, and .
firstclass functions hen , ρ, σn−1 i ⇓ hvn , σn i
he, ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
158 (LET)
hLET(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
ρ0 = ρ σ0 = σ
he1 , ρ0 , σ0 i ⇓ hv1 , σ0′ i ℓ1 ∈
/ dom σ0′ ρ1 = ρ0 {x1 7→ ℓ1 } σ1 = σ0′ {ℓ1 7→ v1 }
..
′
.
hen , ρn−1 , σn−1 i ⇓ hvn , σn−1 i
′ ′
ℓn ∈
/ dom σn−1 ρn = ρn−1 {xn 7→ ℓn } σn = σn−1 {ℓn 7→ vn }
′
he, ρn , σn i ⇓ hv, σ i
hLETſTAR(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
(LETſTAR)
158b. hextend env by sequentially binding es to xs 158bi≡ (157d)
{
Namelist xs;
Explist es;
Finally, before evaluating any expressions, LETREC binds each name to a fresh
location.
The locations’ initial contents are unspecified, and they remain unspecified until
all the values are computed. The right‐hand sides are confirmed to be LAMBDAs at
parse time, by the same function that confirms the xi ’s are distinct, so they needn’t
be checked here.
159a. hextend env by recursively binding es to xs 159ai≡ (157d)
{
Namelist xs;
The control‐flow operations are implemented much as they are in Impcore. The
semantic rules are not worth repeating.
159b. hevaluate e‑>ifx and return the result 159bi≡ (155b 48b)
if (istrue(eval(e‑>ifx.cond, env)))
return eval(e‑>ifx.truex, env);
else
return eval(e‑>ifx.falsex, env);
159c. hevaluate e‑>whilex and return the result 159ci≡ (155b 48b)
while (istrue(eval(e‑>whilex.cond, env)))
eval(e‑>whilex.body, env);
return falsev;
bindalloc 153c
159d. hevaluate e‑>begin and return the result 159di≡ (155b 48b) bindalloclist
{ 153c
type Def A
Value lastval = falsev;
type Echo S293f
for (Explist es = e‑>begin; es; es = es‑>tl)
type Env 153a
lastval = eval(es‑>hd, env); env 155b
return lastval; eval 155a
} evallist S310b
type Explist S309b
falsev S327b
2.12.4 Evaluating true definitions find 153b
istrue 154c
type Namelist
Each true definition is evaluated by function evaldef, which updates the store 43a
and returns a new environment. If echo is ECHOES, evaldef also prints. Function unspecified 154d
evaldef doesn’t handle record definitions; the record form is syntactic sugar, not type Value A
a true definition. type Valuelist
S309c
159e. hevaldef.c 159ei≡
Env evaldef(Def d, Env env, Echo echo) {
switch (d‑>alt) {
case VAL: hevaluate val binding and return new environment 160ai
case EXP: hevaluate expression, assign to it, and return new environment 160bi
case DEFINE: hevaluate function definition and return new environment 160ci
}
assert(0);
}
x ∈ dom ρ
he, ρ, σi ⇓ hv, σ ′ i
2 hVAL(x, e), ρ, σi → hρ, σ ′ {ρ(x) 7→ v}i
(DEFıNEOLDGLOBAL)
x 6∈ dom ρ ℓ 6∈ dom σ
Scheme,
Sexpressions, and he, ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}i ⇓ hv, σ ′ i
(DEFıNENEWGLOBAL)
firstclass functions hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ {ℓ 7→ v}i
160a. hevaluate val binding and return new environment 160ai≡ (159e)
160 {
if (find(d‑>val.name, env) == NULL)
env = bindalloc(d‑>val.name, unspecified(), env);
Value v = eval(d‑>val.exp, env);
*find(d‑>val.name, env) = v;
hif echo calls for printing, print either v or the bound name S311ei
return env;
}
As in Impcore, evaluating a top‐level expression has the same effect on the en‐
vironment as evaluating a definition of it, except that the interpreter always prints
the value, never the name “it.”
160b. hevaluate expression, assign to it, and return new environment 160bi≡ (159e)
{
Value v = eval(d‑>exp, env);
Value *itloc = find(strtoname("it"), env);
hif echo calls for printing, print v S312ai
if (itloc == NULL) {
return bindalloc(strtoname("it"), v, env);
} else {
*itloc = v;
return env;
}
}
Each primitive is associated with a unique tag, which identifies the primitive, and
with a function, which implements the primitive. The tags enable one function to
implement multiple primitives, which makes it easy for similar primitives to share
code. The primitives are implemented by these functions:
Each arithmetic primitive expects two integer arguments, which are obtained by
projecting µScheme values. The projection function projectint32 takes not only
a value but also an expression, so if its argument is not an integer, it can issue an
informative error message.
161a. hprim.c 161ai≡ 161b ▷
static int32_t projectint32(Exp e, Value v) {
§2.12
if (v.alt != NUM)
runerror("in %e, expected an integer, but got %v", e, v);
The interpreter
return v.num; 161
}
Function arith first converts its arguments to integers, then consults the tag
to decide what to do. In each case, it computes a number or a Boolean, which is
converted a µScheme value by either mkNum or mkBool, both of which are gener‐
ated automatically from the definition of Value in code chunk 152a. Checks for
arithmetic overflow are not shown.
161b. hprim.c 161ai+≡ ◁ 161a 161c ▷
Value arith(Exp e, int tag, Valuelist args) {
checkargc(e, 2, lengthVL(args));
int32_t n = projectint32(e, nthVL(args, 0));
int32_t m = projectint32(e, nthVL(args, 1));
switch (tag) {
case PLUS: return mkNum(n + m);
case MINUS: return mkNum(n ‑ m);
case TIMES: return mkNum(n * m); allocate 154a
case DIV: if (m==0) runerror("division by zero"); bindalloc 153c
else return mkNum(divide(n, m)); // round to minus infinity checkargc 47c
echo 159e
case LT: return mkBoolv(n < m);
env 159e
case GT: return mkBoolv(n > m);
eval 155a
default: assert(0); evaldef 155a
} type Exp A
} find 153b
lengthVL A
mkBoolv A
Other binary primitives mkLambdax A
mkNum A
µScheme has two other binary primitives, which don’t require integer arguments: mkPair A
mkVal A
cons and =. The implementation of = is relegated to the Supplement, but the im‐
nthVL A
plementation of cons is shown here. Because S‐expressions are a recursive type, runerror 47a
a cons cell must contain pointers to S‐expressions, not S‐expressions themselves. strtoname 43b
Every cons must therefore allocate fresh locations for the pointers. This behavior unspecified 154d
type Value A
makes cons a major source of allocation in µScheme programs.14 type Valuelist
161c. hprim.c 161ai+≡ ◁ 161b 162a ▷ S309c
Value cons(Value v, Value w) {
return mkPair(allocate(v), allocate(w));
}
14
In full Scheme, a cons cell is typically represented by a pointer to an object allocated on the heap,
so cons requires only one allocation, not two.
Unary primitives
Unary primitives are implemented here. Most of the cases are relegated to the Sup‐
plement.
checkargc(e, 1, lengthVL(args));
Scheme, Value v = nthVL(args, 0);
Sexpressions, and switch (tag) {
firstclass functions case NULLP:
return mkBoolv(v.alt == NIL);
162 case CAR:
if (v.alt == NIL)
runerror("in %e, car applied to empty list", e);
else if (v.alt != PAIR)
runerror("car applied to non‑pair %v in %e", v, e);
return *v.pair.car;
case PRINTU:
if (v.alt != NUM)
runerror("printu applied to non‑number %v in %e", v, e);
print_utf8(v.num);
return v;
case ERROR:
runerror("%v", v);
return v;
hother cases for unary primitives S314ci
default:
assert(0);
}
}
Like Impcore, µScheme is stratified into two layers: a core language and syntactic
sugar (page 68). The core language is defined by the operational semantics and
is implemented by functions eval and evaldef. The syntactic sugar is defined and
implemented by translating it into the core language. In Scheme, the core language
can be very small indeed: even the LET and BEGıN forms can be implemented as
syntactic sugar. (But in µScheme, they are part of the core.) The translations of
LET and BEGıN are shown below, as are the translations used to implement short‐
circuit conditionals, cond, and the record form. These translations introduce two
key programming‐language concepts: captureavoiding substitution and hygiene.
2.13.1 Syntactic sugar for LET forms
This translation works just like any other recursive function—but the recursive
function is applied to syntax, not to values. It looks like this:
163. hparse.c 163i≡
Exp desugarLetStar(Namelist xs, Explist es, Exp body) {
if (xs == NULL || es == NULL) {
assert(xs == NULL && es == NULL);
return body;
} else {
return desugarLet(mkNL(xs‑>hd, NULL), mkEL(es‑>hd, NULL),
desugarLetStar(xs‑>tl, es‑>tl, body));
}
}
The desugared code works just as well as µScheme’s core code—and you can prove it
(Exercises 44 and 45).
Finally, a letrec can be desugared into a let expression that introduces all the
checkargc 47c
variables, which is followed by a sequence of assignments: type Exp A
△ type Explist S309b
(letrec ([x1 e1 ] · · · [xn en ]) e) = lengthVL A
(let ([x1 unspecified] · · · [xn unspecified]) mkBoolv A
(begin (set x1 e1 ) · · · (set xn en ) mkEL A
mkNL A
e)). type Namelist
43a
This translation works only when each ei is a lambda expression, as required by the nthVL A
operational semantics. print_utf8 S188a
runerror 47a
type Value A
2.13.2 Syntactic sugar for cond (Lisp’s original conditional form) type Valuelist
S309c
µScheme’s conditional expression, written using if, allows for only two alterna‐
tives. But real programs often choose among three or more alternatives. For some
such choices, C and the Algol‐like languages offer a switch statement, but it can
choose only among integer values that are known at compile time—typically enu‐
meration literals. A more flexible multi‐way choice is offered by a syntactic form
from McCarthy’s original Lisp: the cond expression. A cond expression contains an
arbitrarily long sequence of questionanswer pairs: one for each choice. I like cond
more than if because cond makes it obvious how many alternatives there are and
what each one is doing.
164a. htranscript for extended µScheme 164ai≡ 164b ▷
‑> (define compare‑numbers (n m)
2 (cond
[(< n m) 'less]
[(= n m) 'equal]
Scheme, [(> n m) 'greater]))
Sexpressions, and ‑> (compare‑numbers 3 2)
firstclass functions greater
‑> (compare‑numbers 3 3)
164 equal
‑> (compare‑numbers 3 4)
less
As a replacement for the primitive function and, which can be called only after
both its arguments are evaluated, µScheme provides a syntactic form &&, which
evaluates its second argument only when necessary. And actually, && can accept
more than two arguments; it is desugared to if expressions as follows:
△
(&& e) =e
△
(&& e1 · · · en ) = (if e1 (&& e2 · · · en ) #f)
µScheme also provides a || form, which is also desugared into if expressions, but
there’s a challenge. The following desugaring doesn’t always work:
△
(|| e1 e2 ) 6= (if e1 #t e2 )
The problem with that right‐hand side is that if e1 is not #f, (|| e1 e2 ) should return
the value of e1 , just as the predefined function or does. But the desugaring into if
returns #t:
164c. htranscript 95ai+≡ ◁ 143d 165a ▷
‑> (or 7 'seven)
7
‑> (if 7 #t 'seven)
#t
When e1 is not #f, a desugaring could return e1 , but this desugaring doesn’t always
work either:
△
(|| e1 e2 ) 6= (if e1 e1 e2 )
This one fails because it could evaluate e1 twice. And if e1 has a side effect, the
desugaring performs the side effect twice, leading to wrong answers. §2.13
165a. htranscript 95ai+≡ ◁ 164c 165b ▷ Extending
‑> (val n 2) µScheme with
‑> (or (< 0 (set n (‑ n 1))) 'finished) syntactic sugar
#t
‑> (val n 2) 165
‑> (if (< 0 (set n (‑ n 1))) (< 0 (set n (‑ n 1))) 'finished)
#f
The or function works because it is a function call: both e1 and e2 are evaluated,
and their results are bound to the formal parameters of the or function. A desug‐
aring could achieve the almost same effect with a let binding:
But binding x doesn’t always work; if x is used in e2 , the desugaring can fail:
165c. htranscript 95ai+≡ ◁ 165b 185b ▷
‑> (val n 0)
‑> (val x 'finished)
‑> (or (< 0 n) x)
finished
‑> (val n 0)
‑> (val x 'finished)
‑> (let ([x (< 0 n)]) (if x x x))
#f
This failure has a name: the global variable x is said to be captured by the desugar‐
ing. To avoid capturing x, it is sufficient to choose some name x that doesn’t appear
car P 162a
in e2 . Such an x is called fresh. So finally, the following desugaring always works:
cdr P 162a
△ cons P S313d
(|| e1 e2 ) = (let ([x e1 ]) (if x x e2 )), provided x does not appear in e2 .
mod B
null? P 162a
Using this idea, the general rules for desugaring || are as follows: or B
△
(|| e) =e
△
(|| e1 · · · en ) = (let ([x e1 ]) (if x x (|| e2 · · · en ))),
where x does not appear in any ei .
Variable capture and fresh names are perennial issues in programming languages.
• In Prolog, when the system is trying to answer a query or prove a goal, sub‐
goals are spawned using substitution.
Substitution algorithms that avoid variable capture are called hygienic. They are
hard to get right. But if you are interested in programming‐language foundations,
you need to understand them. And if you are interested in language design, you
need to understand that using a language based on substitution is not fun. (As ex‐
amples, I submit TEX and Tcl.) The more different contexts in which you see sub‐
stitution, the better you will understand it.
Let’s use substitution to take a second look at µScheme’s conditional expres‐
sions. The expression (&& e1 e2 ) is desugared into an if expression in three steps:
1. The replacement will be derived from the template expression (if 1 2 #f).
Mathematically, the symbols 1 and 2 are ordinary program variables; the
notation telegraphs an intention to substitute for them.
The original expression and its replacement have the same semantics:
Every choice of x determines a template, and any given e1 and e2 can be desugared
using any template in which x does not appear in e2 .
The same idea of hygiene—choosing a variable in a template that does not interfere
with what is substituted—is used in the rules for desugaring BEGıN:
△
(begin e) =e
△
(begin e1 · · · en ) = (let ([x e1 ]) (begin e2 · · · en )),
where x does not appear in any ei .
Like Lisp before it, Scheme has split into dialects. The model for µScheme is the
1998 R5 RS standard, which embodies the minimalist design philosophy of the orig‐
inal Scheme. The 2007 R6 RS standard defines a bigger, more complicated Scheme,
which most R5 RS implementations chose not to adopt. The subsequent R7 RS stan‐
dard splits Scheme into two languages: a small one, finalized in 2013, that more
closely resembles the original, and a big one, still incomplete as of 2022, that is
believed to be better suited to mainstream software development. And while stan‐
dards have their advantages, many Schemers prefer Racket, a nonstandard dialect
that benefits from a talented team of developers and contributors. But most di‐
alects share aspects that I consider interesting, impressive, or relevant for some‐
one making a transition from µScheme—which is the topic of this section.
Let’s begin with some minor lexical and syntactic differences. Through 2007, iden‐
tifiers and symbols in full Scheme were not case‐sensitive; for example, 'Foo was
15
The real story is more complicated: instead of using names cons, pair?, car, and so on, the desug‐
aring creates literals that refer directly to those primitives.
the same as 'foo. But as of the 2007 R6 RS standard—to give it its full name, the
Revised6 Report on Scheme (Sperber et al. 2009)—identifiers and symbols are case‐
sensitive, as in µScheme.
Full Scheme uses define to introduce all top‐level bindings, with slightly dif‐
ferent concrete syntax from µScheme:
A result from assoc can be tested directly in an if expression, and if not #f, it can
be updated in place by primitive function set‑cdr!.
Full Scheme includes the other list functions found in µScheme, but often un‐
der slightly different names, such as for‑all, exists, fold‑left, and fold‑right.
And the full Scheme functions are more general: they can operate on any number
of lists simultaneously.
Full Scheme has an additional quoting mechanism: using quasiquotation, you
can splice computed values or lists into quoted S‐expressions (Exercise 55).
In full Scheme, a function can take a variable number of arguments. Either
the function takes one formal parameter, which is the whole list of arguments, or a
formal parameter separated by a trailing period is bound to any “extra” arguments:
169b. hR RS Scheme transcript 169ai+≡
6
◁ 169a
> ((lambda (x y . zs) zs) 3 4 5 6) ;; Racket's r6rs Scheme
'(5 6)
> ((lambda xs xs) 3 4 5 6)
'(3 4 5 6)
In addition, many primitive functions and macros, such as +, <, and, max, etc., ac‐
cept an arbitrary number of arguments—even zero.
In full Scheme, the syntactic form set is called set!. And full Scheme can
mutate the contents of cons cells, not just variables, using primitive functions
set‑car! and set‑cdr! (Exercises 50 and 58).
Finally, in full Scheme, the order in which expressions are evaluated is usually
unspecified. To enforce a particular order of evaluation, use let* or letrec*.
2.14.2 Proper tail calls (upcoming in Chapter 3)
2 with arguments,” that is, it does not push anything on the call stack (Steele 1977).
An arbitrarily long sequence of tail calls takes no more space than one ordinary
call.
Scheme,
Intuitively, a call is a tail call if it is the last thing a function does, i.e., the result
Sexpressions, and
of the tail call is also the result of the calling function. As an important special
firstclass functions
case, proper tail recursion requires that if the last thing a full Scheme function does
170 is make a recursive call to itself, the implementation makes that recursive call as
efficient as a goto.
Many tail calls are easy to identify; for example, in a C program, the last call
before a return or before the end of a function is a tail call. And in a C statement
return f(args ), the call to f is a tail call. To identify all tail calls, however, we need
a more precise definition.
In Scheme, a tail call is a function call that occurs in a tail context. Tail contexts
are defined by induction over abstract syntax. The full story is told by in Kelsey,
Clinger, and Rees (1998, Section 3.5), from whom I have adapted this account, but
the key rules look like this:
The following example shows one tail call, to f. The calls to g and h are not tail
calls. The reference to x is in a tail context, but it is not a call and so is not a tail
call.
(lambda ()
(if (g)
(let ([x (h)])
x)
(if (g) (f) #f)))
Full Scheme has more primitive data types and functions than µScheme. These
types include mutable vectors (arrays), which can be written literally using the
#(…) notation, as well as characters, mutable strings, and “I/O ports.” And the
R6 RS and R7 RS standards include records with named fields; µScheme’s record
form (Section 2.13.6) is a scaled‐down version of R6 RS records.
Full Scheme supports lazy computations with delay and force.
Full Scheme provides many types of numbers, the meanings and representa‐
tions of which are carefully specified in the standard. Numeric types can be ar‐
ranged in a tower, in which each level contains all the levels below it:
number
complex
real
rational §2.14
integer Scheme as it
Numbers may also be exact or inexact. Most Scheme implementations, for exam‐ really is
ple, automatically do exact arithmetic on arbitrarily large integers (“bignums”).
171
If you are curious about bignums, you can implement them yourself; do Exer‐
cises 49 and 50 in Chapter 9 or Exercises 37 to 39 in Chapter 10.
Full Scheme includes not only the syntactic sugar described in Section 2.13 but also
the ability for programmers to define new forms of syntactic sugar, called macros
(or syntactic abstractions). Macros have the same status as any other Scheme code:
they can be included in user code and in libraries, and anybody can define one by
writing a macro transformer. And Scheme’s macros are hygienic:
• If a macro transformer inserts a binding for an identifier (variable or key‐
word) not appearing in the macro use, the identifier is in effect renamed
throughout its scope to avoid conflicts with other identifiers. For example,
just as prescribed by the rules in Section 2.13.3, if a macro transformer ex‐
pands (|| e1 e2 ) into (let ([t e1 ]) (if t t e2 )), then t is automatically re‐
named to avoid conflicting with identifiers in e1 and e2 . Standard Scheme
does something similar with or; the expression (or e1 e2 ) is defined to ex‐
pand to (let ([x e1 ]) (if x x e2 )). Scheme’s macro facility renames x as
needed to avoid capture.
• Hygiene also protects each macro’s unbound names. For example, µScheme’s
record definition desugars into definitions that use cons, car, cdr, and pair?
(Section 2.13.6). These names must not take their meanings from the context
in which the record definition appears, as shown by this contrived example,
which mixes µScheme with full Scheme:
If record were desugared naïvely, the code for point? would use the ver‐
sion of pair? that’s in scope, and the assertion would fail. But the asser‐
tion (point? (make‑point e1 e2 )) should always succeed. And in a truly
hygienic macro system, it does: the hygienic macro system guarantees that
when a macro is desugared, its free names refer to the bindings visible where
the macro was defined, regardless of how those names may be bound where
the macro is used.
Hygiene makes Scheme macros remarkably powerful. Macro transformers can de‐
fine new language features that in most settings that would require new abstract
syntax. A good implementation of Scheme is more than just a programming lan‐
guage; it is a system for crafting programming languages. Details, examples, and
ideas can be found in some of the readings mentioned in Section 2.15.2.
2.14.5 call/cc (also upcoming in Chapter 3)
Full Scheme includes a primitive function which can capture continuations that
are defined implicitly within the interpreter, as if a program had been written
ATOM A Scheme value that can be compared for equality in constant time: a sym‐
bol, a number, a Boolean, or the empty list. Atoms are the base case in the
definition of S‐EXPREſſıONſ.
FREE VARıABLE A name appearing in the body of a function that is bound neither
by the LAMBDA ABſTRACTıON that introduces the function nor by any LET
BıNDıNG within the body of the function. Names that refer to primitives like §2.15
+ and cons are typically free variables. LOCATıONſ of free variables are cap‐ Summary
tured in CLOſUREſ. 173
HıGHER‐ORDER FUNCTıON A function that either takes one or more functions as
arguments, or more interestingly, that returns one or more functions as re‐
sults. Classic examples include MAP, FıLTER, and FOLD. More interesting
higher‐order functions can be defined only in a language in which functions
are both FıRſT‐CLAſſ and NEſTED. Compare with FıRſT‐ORDER FUNCTıON.
LAMBDA ABſTRACTıON The syntactic form by which a function is introduced.
Scheme functions need not be named.
VALUE ſEMANTıCſ A semantics in which a variable stands for a value, not a muta‐
ble LOCATıON. Describes languages like Impcore, ML, and Haskell. Contrast
with LOCATıON ſEMANTıCſ.
For insight into how a language is born, John McCarthy’s original paper (1960) and
book (1962) about Lisp are well worth reading. But some later treatments of Lisp
are clearer and more complete; these include books by Touretzky (1984), Wilen‐
sky (1986), Winston and Horn (1984), Graham (1993), and Friedman and Felleisen
(1996). For the serious Lisper, the Common Lisp manual (Steele 1984) is an invalu‐
able reference.
For Scheme, the closest analog is the “Lambda: The Ultimate —” series by Steele
and Sussman (Sussman and Steele 1975; Steele and Sussman 1976, 1978); I espe‐
cially recommend the 1978 article. For reference, Dybvig’s (1987) book is clear and
well organized, and there are always the official standards (Kelsey, Clinger, and
Rees 1998; Sperber et al. 2009; Shinn, Cowan, and Gleckler 2013).
Recursion isn’t just for functional programmers; it is also highly regarded in
procedural languages, as taught by Rohl (1984), Roberts (1986), and Reingold and
Reingold (1988). Proper tail recursion is precisely defined by Clinger (1998), who
also explores some of the implications.
To argue that functional programming matters, Hughes (1989) shows that it
provides superior ways of putting together code: tools like map, filter, and
foldr enable us to combine small, simple functions into big, powerful func‐
tions. By Hughes’s standard, µScheme is only half a functional language: al‐
though µScheme provides higher‐order functions, it does not provide lazy evalu
ation, a technique now strongly associated with Haskell.
Algebraic laws are explored in depth by Bird and Wadler (1988), who include
many more list laws than I present. Algebraic laws are also a great tool for specify‐
§2.15
ing the behavior of abstract data types (Liskov and Guttag 1986). The algebraic ap‐
Summary
proach can also be used on procedural programs, albeit with some difficulty (Hoare
et al. 1987). And algebraic laws support a systematic, effective, propertybased ap‐ 175
proach to software testing (Claessen and Hughes 2000).
Full Scheme can express a shocking range of programming idioms, algorithms,
data structures, and other computer‐science ideas; the demonstration by Abelson
and Sussman (1985) is likely to impress you. Beginners will get more out of Har‐
vey and Wright (1994), who aim at students with little programming experience.
Another approach for beginners uses five subsets of Scheme, which are carefully
crafted to help raw beginners evolve into successful Schemers (Felleisen et al. 2018).
The idea of using statically scoped closures to implement first‐class, nested
functions did not originate with Scheme. The idea had been developed in a num‐
ber of earlier languages, mostly in Europe. Examples include Iswim (Landin 1966),
Pop‐2 (Burstall, Collins, and Popplestone 1971), and Hope (Burstall, MacQueen,
and Sannella 1980). The book by Henderson (1980) is from this school. Also highly
recommended is the short, but very interesting, book by Burge (1975).
Continuation‐passing style was used by Reynolds (1972) to make the meanings
of “definitional” interpreters independent of their implementation language. The
continuation‐passing backtracking search in Section 2.10 is based on a “Byrd box,”
which was used to understand Prolog programs (Byrd 1980); my terminology is that
of Proebsting (1997), who describes an implementation of Icon, which has back‐
tracking built in (Griswold and Griswold 1996).
Macros are nicely demonstrated by Flatt (2012), who creates new languages
using Scheme’s syntactic abstraction together with other tools unique to Racket.
If you like the ideas and you want to define your own macros, continue with Hen‐
dershott’s (2020) tutorial.
Macros have a long history. Kohlbecker et al. (1986) first addressed the problem
of variable capture; they introduce a hygiene condition sufficient to avoid variable
capture, and they define a hygienic macro expander. Dybvig, Hieb, and Brugge‐
man (1992) build on this work, reducing the cost of macro expansion and enabling
macros to track source‐code locations; a key element of their macro expander is
the syntax object, which encapsulates not only a fragment of abstract‐syntax tree
but also some information about its environment, so that variable capture can be
avoided. Moving from full Scheme to Racket, Flatt et al. (2012) further increase the
power of the macro system by enabling macro expanders to share information. But
the complexity of the system is acknowledged as a drawback; in particular, it is no
longer so obvious that variable capture is always avoided. Flatt (2016) proposes a
new, simpler model wherein a fragment of syntax is associated with a set of envi‐
ronments, which together determine the meaning of each name mentioned within
the fragment.
2.16 EXERCıſEſ
The exercises are summarized in Table 2.8. Some of the highlights are as follows:
Scheme, • Exercises 22 and 24 ask you to prove some classic algebraic laws of pure func‐
Sexpressions, and tional programming: append is associative, and the composition of maps is
firstclass functions the map of the composition. The laws can be used to improve programs,
sometimes even by optimizing compilers. The proofs combine equational
176 reasoning with induction.
• Exercise 38 asks you to implement a “data structure” whose values are repre‐
sented as functions.
• Exercise 60 on page 198 asks you to add a trace facility to the µScheme inter‐
preter; it will help you master the C code for the evaluator.
As you tackle the exercises, you can refer to Table 2.3 on page 97, which lists all
the functions in µScheme’s initial basis.
Q. A search function takes two continuations: one for success and one for failure.
Which continuation expects a parameter? Why?
R. In Impcore, a variable stands for a value v , but in µScheme, a variable stands
for a location ℓ. What example in the chapter exploits this aspect of µScheme’s
semantics?
S. The equation (|| e1 e2 ) = (if e1 e1 e2 ) is not quite a valid algebraic law.
What could go wrong?
T. The equation (|| e1 e2 ) = (let ([x e1 ]) (if x x e2 )) is not quite a valid alge‐
braic law. What could go wrong?
2.16.2 Functions that consume lists
(b) When both xs and ys are in set LIST (ATOM ), sublist? determines
whether the first list is a mathematical subsequence of the second.
That is, (sublist? xs ys) returns #t if and only if the list ys contains
the elements of xs, in the same order, but possibly with other values in
between.
hexercise transcripts 178ai+≡
‑> (sublist? '(a b c) '(x a y b z c))
#t
‑> (sublist? '(a y b) '(x a y b z c))
#t
‑> (sublist? '(a z b) '(x a y b z c))
#f
‑> (sublist? '(x y z) '(x a y b z c))
#t
(a) (remove x s) returns a set having the same elements as set s with ele‐
ment x removed.
(b) (subset? s1 s2) determines if s1 is a subset of s2.
(c) (=set? s1 s2) determines if lists s1 and s2 represent the same set.
4. Sets as lists: Understanding equality. Chunks 105a and 105c use lists to rep‐
resent sets, and chunk 105d shows an example in which a set’s element may
also be a list. In the text, I claim that if member? uses = instead of equal?,
the example in chunk 105d doesn’t work.
(b) What goes wrong exactly, and why should the fault be attributed to us‐
§2.16
ing the = primitive instead of the equal? function?
Exercises
Informally, mirror returns the S‐expression you would get if you looked
at its argument in a vertically oriented mirror, except that the individ‐
ual atoms are not reversed. (Try putting a mirror to the right of the ex‐
ample, facing left.) More precisely, mirror consumes an S‐expression
and returns the S‐expression that you would get if you wrote the brack‐
ets and atoms of the original S‐expression in reverse order, exchanging
open brackets for close brackets and vice versa.
(d) Function flatten consumes a list of S‐expressions and erases internal
brackets. That is, when xs is a list of S‐expressions, (flatten xs) con‐
structs a list having the same atoms as xs in the same order, but in a
flat list. For purposes of this exercise, '() should be considered not as
an atom but as an empty list of atoms.
hexercise transcripts 178ai+≡
‑> (flatten '((I Ching) (U Thant) (E Coli)))
(I Ching U Thant E Coli)
‑> (flatten '(((((a))))))
(a)
hexercise transcripts 178ai+≡
‑> (flatten '())
()
‑> (flatten '((a) () ((b c) d e)))
(a b c d e)
The theorem implies that if you are writing a recursive function that con‐
sumes a list of S‐expressions, you could instead try to write a more gen‐
eral function that consumes any S‐expression. The more general function
is sometimes simpler.
(a) Define a function desserts that takes a list of frozen dinners and re‐
turns a list of the desserts.
(b) Using frozen‑dinner?, define a function #dinners that takes a list of
values and returns the number of values that are frozen dinners.
(c) Define a function steak‑dinners that takes a list of frozen dinners and
returns a list containing only those frozen dinners that offer 'steak as
a protein.
11. Using node records to implement tree traversals. Using the representation de‐
fined in Section 2.4, define functions that implement postorder and inorder
traversal for binary trees.
12. Traversals of rose trees. A rose tree is a tree in which each node can have arbi‐
trarily many children. We can define an abstract rose tree as a member of
the smallest set that satisfies this recursion equation:
Extend the preorder and level‐order traversals of Sections 2.4.2 and 2.6 to
rose trees. Note that in the non‐abstract ROSE representation, a leaf node
labeled a can be represented in two ways: either as 'a or as '(a).
13. Efficient queues. The implementation of queues in Section 2.6 (page 119) is
simple but inefficient; enqueue can take time and space proportional to the
number of elements in the queue. The problem is the representation: the
queue operations have to get to both ends of the queue, and getting to the
back of a list requires linear time. A better representation uses two lists
stored in a record:
2 In this record,
Scheme, • The queue‑front list represents the front of the queue. It stores older
Sexpressions, and elements, and they are ordered with the oldest element at the begin‐
firstclass functions ning, so functions front and without‑front can be implemented us‐
ing car and cdr.
182
• The queue‑back list represents the back of the queue. It stores young
elements, and they are ordered with the youngest element at the begin‐
ning, so function enqueue can be implemented using cons.
The only trick here is that when the queue‑front elements are exhausted,
the queue‑back elements must somehow be transferred to the front. Us‐
ing the two lists, implement value emptyqueue and functions empty?, front,
without‑front, and enqueue. Each operation should take constant time on
average. (Proofs of average‐case time over a sequence of operations use
amortized analysis.)
14. Algebraic laws for queues. The inefficient queue operations in Section 2.6 and
the efficient queue operations in Exercise 13 can both be described by a sin‐
gle set of algebraic laws. As explained in Section 2.5.2, such laws must specify
the result of applying any acceptable observer to any combination of con‐
structors. (The queues in this chapter are immutable, so mutators do not
come into play.)
Write algebraic laws sufficient to specify the behavior of all meaningful com‐
binations of constructors and observers. Don’t try to specify erroneous com‐
binations like (front emptyqueue).
Hint: This exercise is harder than it may appear—if you’re not careful, you
may find yourself specifying a stack, not a queue. Consider turning each
algebraic law into a function, so you can test it as shown in Exercise 36.
15. Graphs represented as Sexpressions: Topological sort. Many directed graphs can
be represented as ordinary S‐expressions. For example, if each node is la‐
beled with a distinct symbol, a graph can be represented as a list of edges,
where each edge is a list containing the labels of that edge’s source and des‐
tination. Write a function that topologically sorts a graph specified by this
edgelist representation. The function (tsort edges) should return a list that
contains the labels in edges, in topological order.
In topological sorting, the two symbols in an edge introduce a precedence con
straint: for example, if '(a b) is an edge, then in the final sorted list of la‐
bels, a must precede b. As an example, (tsort '((a b) (a c) (c b) (d b)))
can return either '(a d c b) or '(a c d b).
Not every graph can be topologically sorted; if the graph has a cycle, function
tsort should call error.
For details on topological sorting, see Sedgewick (1988, Chapter 32), or Knuth
(1973, Section 2.2.3).
hexercise transcripts 178ai+≡
‑> (tsort '((duke commoner) (king duke) (queen duke) (country king)))
(queen country king duke commoner)
§2.16
Exercises
2.16.7 Equational reasoning with firstorder functions
183
16. Prove that (member? x emptyset) = #f.
17. Prove that for any predicate p?, (exists? p? '()) = #f.
18. Prove that for any list of values xs, (all? (lambda (_) #t) xs) = #t.
22. Prove that (append (append xs ys) zs) = (append xs (append ys zs)).
23. Prove that (flatten (mirror xs)) = (reverse (flatten xs)), where func‐
tions flatten and mirror are defined as in Exercise 8.
24. Prove that the composition of maps is the map of the composition:
25. Prove again that the composition of maps is the map of the composition, but
this time, prove equality of two functions, not just equality of two lists:
To prove that two functions are equal, show that when applied to equal argu‐
ments, they always return equal results.
26. Prove that if takewhile and dropwhile are defined as in Exercise 31, then for
any list xs, (append (takewhile p? xs) (dropwhile p? xs)) = xs.
(a) cdr*, which lists the cdr’s of each element of a list of lists:
29. Maps and folds with list results. Use map, curry, foldl, and foldr to define the
following functions:
30. Folds for everything. Use foldr or foldl to implement map, filter, exists?,
and all?. It is OK if some of these functions do more work than their official
versions, as long as they produce the same answers.
For the best possible solution, define functions that no civilized µScheme
program can distinguish from the originals. (A civilized program may exe‐
cute any code, including set, but it may not change the functions in the initial
basis.)
2.16.10 Functions as arguments
31. Selections of sublists. Function takewhile takes a predicate and a list and re‐
turns the longest prefix of the list in which every element satisfies the pred‐
icate. Function dropwhile removes the longest prefix and returns whatever
is left over.
hexercise transcripts 178ai+≡
‑> (define even? (x) (= (mod x 2) 0))
‑> (takewhile even? '(2 4 6 7 8 10 12))
§2.16
(2 4 6) Exercises
‑> (dropwhile even? '(2 4 6 7 8 10 12))
185
(7 8 10 12)
34. Generalized dot product. Exercise 5 asks for a dot‑product function. General‐
ize this pattern of computation into a “fold‐like” function foldr‑pair, which
operates on two lists of the same length. (This function is primitive in APL.)
35. Generalized preorder traversal. Function preorder in Section 2.4.2 is not so
useful if, for example, one wants to perform some other computation on
a tree, like finding its height. By analogy with foldl and foldr, define
fold‑preorder. In addition to the tree, it should take two arguments: a func‐
36. Generalized all?, with predicates of two or three arguments. In this problem
you generalize function all? so it can work with all pairs from two lists or
all triples from three lists. For example, all‑pairs? might be used with this
length‑append‑law function to confirm that the law holds for 25 combina‐
tions of inputs:
hexercise transcripts 178ai+≡
‑> (define length‑append‑law (xs ys)
(= (length (append xs ys))
(+ (length xs) (length ys))))
‑> (all‑pairs? length‑append‑law
'((a b c) (singleton) () (3 1 4 1 5 9) (2 7 1 8 2 8))
'((1 2 3) () (elephants got big feet) (z) (w x)))
#t
(a) Define function all‑pairs?, which tests its first argument on all pairs
of values taken from its second two arguments. Here are some more
example calls:
hexercise transcripts 178ai+≡
‑> (all‑pairs? < '(1 2 3) '(4 5 6 7))
#t
‑> (all‑pairs? < '(1 2 3) '(4 5 6 2))
#f
When the second test fails, the fault could be in the implementation or
the specification. To simplify diagnosis, I expand the < function:
hexercise transcripts 178ai+≡
‑> (all‑pairs? (lambda (n m) (< m n)) '(1 2 3) '(4 5 6 2))
#f
It is definitely not an algebraic law that for all m and n, m < n, so the
fault here lies in the specification, not in the implementation of <.
(b) Define function all‑triples?, which works like all‑pairs? but can
test laws like the associativity of append.
hexercise transcripts 178ai+≡
‑> (define a‑a‑law (xs ys zs) ;; append/append law
(equal? (append xs (append ys zs))
(append (append xs ys) zs)))
‑> (all‑triples? a‑a‑law '((a) () (b c)) '((1 2) (3)) '((4 5 6)))
#t
§2.16
2.16.11 Functions as results
Exercises
37. Using lambda: Creation and combination of faultdetection functions. This prob‐ 187
lem models a real‐life fault detector for Web input. An input to a Web form
is represented as an association list, and a detector is a function that takes an
input as argument, then returns a (possibly empty) list of faults. A fault is
represented as a symbol—typically the name of an input field that is unac‐
ceptable.
Define the following functions, each of which is either a detector or a detec‐
tor builder:
(a) Function faults/none is a detector that always returns the empty list
of faults.
(b) Function faults/always takes one argument (a fault F ), and it returns
a detector that finds fault F in every input.
(c) Function faults/equal takes two arguments, a key k and a value v , and
it returns a detector that finds fault k if the input binds k to v . Otherwise
it finds no faults.
(d) Function faults/union takes two detectors d1 and d2 as arguments.
It returns a detector that, when applied to an input, returns all the faults
found by detector d1 and also all the faults found by detector d2 .
38. Sets represented as functions. In just about any language, sets can be repre‐
sented as lists, but in Scheme, because functions are first class, a set can be
represented as a function. This function, called the characteristic function,
is the function that returns #t when given an element of the set and #f oth‐
erwise. For example, the empty set is represented by a function that always
returns #f, and membership test is by function application:
hexercise transcripts 178ai+≡
‑> (val emptyset (lambda (x) #f))
‑> (define member? (x s) (s x))
append B 99
Representing each set by its characteristic function, solve the following prob‐ cons P S313d
lems: equal? B 104
max B
(a) Define set evens, which contains all the even integers.
(b) Define set two‑digits, which contains all two‐digit (positive) numbers.
(c) Implement add‑element, union, inter, and diff. The set (diff s1 s2)
is the set that contains every element of s1 that is not also in s2.
(d) Implement the third style of polymorphism (page 134).
39. Lists reimplemented using just lambda. This exercise explores the power of
lambda, which can do more than you might have expected.
(a) I claim above that any implementation of cons, car, and cdr is accept‐
able provided it satisfies the list laws in Section 2.5.1. Define cons, car,
cdr, and empty‑list using only if, lambda, function application, the
primitive =, and the literals #t and #f. Your implementations should
pass this test:
hexercise transcripts 178ai+≡
2 ‑> (define nth (n xs)
(if (= n 1)
(car xs)
Scheme, (nth (‑ n 1) (cdr xs))))
Sexpressions, and nth
firstclass functions ‑> (val ordinals (cons '1st (cons '2nd (cons '3rd empty‑list))))
‑> (nth 2 ordinals)
188 2nd
‑> (nth 3 ordinals)
3rd
40. Mutable reference cells implemented using lambda. Exercise 58 invites you to
add mutation to the µScheme interpreter by adding primitives set‑car! and
set‑cdr! But if you want to program with mutation, you don’t need new
primitives—lambda is enough.
A mutable container that holds one value is called a mutable reference cell. De‐
sign a representation of mutable reference cells in µScheme, and implement
in µScheme, without modifying the interpreter, these new functions:
(a) Function make‑ref takes one argument v and returns a fresh, mutable
reference cell that initially contains v . The mutable reference cell re‐
turned by make‑ref is distinct from all other mutable locations.
(b) Function ref‑get takes one argument, which is a mutable reference
cell, and returns its current contents.
(c) Function ref‑set! takes two arguments, a mutable reference cell and
a value, and it updates the mutable reference cell so it holds the value.
It also returns the value.
hexercise transcripts 178ai+≡
‑> (val r (make‑ref 3))
‑> (ref‑get r)
3
‑> (ref‑set! r 99)
99
‑> (ref‑get r)
99
hexercise transcripts 178ai+≡
‑> (define inc (r)
(ref‑set! r (+ 1 (ref‑get r))))
‑> (inc r)
100
2.16.12 Continuations
§2.16
41. Continuationpassing style for a Booleanformula solver. Generalize the solver in Exercises
Section 2.10.2 to handle any formula, where a formula is one of the following:
189
• A symbol, which stands for a variable
• The list (not f ), where f is a formula
• The list (and f1 . . . fn ), where f1 , . . . , fn are formulas
• The list (or f1 . . . fn ), where f1 , . . . , fn are formulas
Mathematically, the set of formulas F is the smallest set satisfying this equa‐
tion:
F = SYM
∪ { (list2 'not f ) | f ∈ F }
∪ { (cons 'and fs ) | fs ∈ LIST (F ) }
∪ { (cons 'or fs ) | fs ∈ LIST (F ) }.
Define function find‑formula‑true‑asst, which, given a satisfiable for‐
mula in this form, finds a satisfying assignment—that is, a mapping of vari‐
ables to Booleans that makes the formula true. Remember De Morgan’s laws,
one of which is mentioned on page 131.
Function find‑formula‑true‑asst should expect three arguments: a for‐
mula, a failure continuation, and a success continuation. When it is called,
as in (find‑formula‑true‑asst f fail succ), it should try to find a satisfy‐
ing assignment for formula f. If it finds a satisfying assignment, it should
call succ, passing both the satisfying assignment (as an association list) and
a resume continuation. If it fails to find a satisfying assignment, it should
call (fail).
You’ll be able to use the ideas in Section 2.10.2, but probably not the code. In‐
stead, try using letrec to define the following mutually recursive functions:
MĸCLOſURE
x1 , . . . , xn all distinct
hLAMBDA(hx1 , . . . , xn i, e), ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, e), ρ |), σi
APPLYCLOſURE
ℓ1 , . . . , ℓn ∈/ dom σn (and all distinct)
he, ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, ec ), ρc |), σ0 i
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ, σn−1 i ⇓ hvn , σn i
hec , ρc {x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
hAPPLY(e, e1 , . . . , en ), ρ, σi ⇓ hv, σ ′ i
LıTERAL
hLıTERAL(v), ρ, σi ⇓ hv, σi
IFTRUE
he1 , ρ, σi ⇓ hv1 , σ ′ i v1 6= BOOLV(#f) he2 , ρ, σ ′ i ⇓ hv2 , σ ′′ i
hıF(e1 , e2 , e3 ), ρ, σi ⇓ hv2 , σ ′′ i
IFFALſE
he1 , ρ, σi ⇓ hv1 , σ ′ i v1 = BOOLV(#f) he3 , ρ, σ ′ i ⇓ hv3 , σ ′′ i
hıF(e1 , e2 , e3 ), ρ, σi ⇓ hv3 , σ ′′ i
WHıLEITERATE
he1 , ρ, σi ⇓ hv1 , σ ′ i v1 6= BOOLV(#f)
he2 , ρ, σ ′ i⇓ hv2 , σ ′′ i hWHıLE(e1 , e2 ), ρ, σ ′′ i ⇓ hv3 , σ ′′′ i
hWHıLE(e1 , e2 ), ρ, σi ⇓ hv3 , σ ′′′ i
WHıLEEND
he1 , ρ, σi ⇓ hv1 , σ ′ i v1 = BOOLV(#f)
hWHıLE(e1 , e2 ), ρ, σi ⇓ hBOOLV(#f), σ ′ i
LET §2.16
x1 , . . . , xn all distinct Exercises
ℓ1 , . . . , ℓn ∈/ dom σn (and all distinct)
σ0 = σ 191
he1 , ρ, σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ, σn−1 i ⇓ hvn , σn i
he, ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ ′ i
hLET(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
LETſTAR
ρ0 = ρ σ0 = σ
he1 , ρ0 , σ0 i ⇓ hv1 , σ0′ i ℓ1 ∈
/ dom σ0′ ρ1 = ρ0 {x1 7→ ℓ1 } σ1 = σ0′ {ℓ1 7→ v1 }
..
′ .
hen , ρn−1 , σn−1 i ⇓ hvn , σn−1 i
′ ′
ℓn ∈
/ dom σn−1 ρn = ρn−1 {xn 7→ ℓn } σn = σn−1 {ℓn 7→ vn }
′
he, ρn , σn i ⇓ hv, σ i
hLETſTAR(hx1 , e1 , . . . , xn , en i, e), ρ, σi ⇓ hv, σ ′ i
LETREC
ℓ1 , . . . , ℓ n ∈
/ dom σ (and all distinct)
x1 , . . . , xn all distinct
ei has the form LAMBDA(· · ·), 1 ≤ i ≤ n
ρ′ = ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }
σ0 = σ{ℓ1 7→ unspecified, . . . , ℓn 7→ unspecified}
he1 , ρ′ , σ0 i ⇓ hv1 , σ1 i
..
.
hen , ρ′ , σn−1 i ⇓ hvn , σn i
he, ρ′ , σ
n {ℓ1 7→ v1 , . . . , ℓn 7→ vn }i ⇓ hv, σ i
′
APPLYEQTRUE
he, ρ, σ0 i ⇓ hPRıMıTıVE(=), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
v1 ≡ v2
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hBOOLV(#t), σ3 i
APPLYEQFALſE
he, ρ, σ0 i ⇓ hPRıMıTıVE(=), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
v1 6≡ v2 (i.e., no proof of v1 ≡ v2 )
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hBOOLV(#f), σ3 i
APPLYPRıNTLN
he, ρ, σ0 i ⇓ hPRıMıTıVE(println), σ1 i
he1 , ρ, σ1 i ⇓ hv, σ2 i
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hv, σ2 i while printing v
APPLYPRıNTU
he, ρ, σ0 i ⇓ hPRıMıTıVE(printu), σ1 i
he1 , ρ, σ1 i ⇓ hNUMBER(n), σ2 i
0 ≤ n < 216
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hNUMBER(n), σ2 i while printing the UTF‐8 coding of n
CONſ
he, ρ, σ0 i ⇓ hPRıMıTıVE(cons), σ1 i
he1 , ρ, σ1 i ⇓ hv1 , σ2 i
he2 , ρ, σ2 i ⇓ hv2 , σ3 i
ℓ1 ∈ / dom σ3 ℓ2 ∈
/ dom σ3 ℓ1 6= ℓ2
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hPAıRhℓ1 , ℓ2 i, σ3 {ℓ1 7→ v1 , ℓ2 7→ v2 }i
CAR CDR
he, ρ, σ0 i ⇓ hPRıMıTıVE(car), σ1 i he, ρ, σ0 i ⇓ hPRıMıTıVE(cdr), σ1 i
he1 , ρ, σ1 i ⇓ hPAıRhℓ1 , ℓ2 i, σ2 i he1 , ρ, σ1 i ⇓ hPAıRhℓ1 , ℓ2 i, σ2 i
hAPPLY(e, e1 ), ρ, σ0 i ⇓ hσ2 (ℓ1 ), σ2 i hAPPLY(e, e1 ), ρ, σ0 i ⇓ hσ2 (ℓ2 ), σ2 i
To help you with the operational‐semantics exercises, the rules of µScheme’s oper‐
ational semantics are summarized in Figures 2.9 to 2.11 on pages 190 to 192.
42. Proof or refutation of algebraic laws for cdr. Algebraic laws can often be proven
by appeal to other algebraic laws, but eventually some proofs have to ap‐
peal to the operational semantics. This exercise explores the algebraic law
for cdr. §2.16
Exercises
(a) The operational semantics for µScheme includes rules for cons, car,
and cdr. Assuming that x and xs are variables and are defined in ρ, use 193
the operational semantics to prove that
(b) Use the operational semantics to prove or disprove the following con‐
jecture: if e1 and e2 are arbitrary expressions, then in any context in
which the evaluation of e1 terminates and the evaluation of e2 termi‐
nates, the evaluation of (cdr (cons e1 e2 )) terminates, and
(cdr (cons e1 e2 )) = e2
The conjecture says that in any state, for any e1 and e2 , evaluating
(cdr (cons e1 e2 )) produces the same value as evaluating e2 would
have.
43. Proof of an algebraic law for if. µScheme’s if expressions participate in many
algebraic laws. Use the operational semantics to prove that
Using the operational semantics, prove that the claim is a good one for the
case where n = 1. That is, prove that for any x1 , e1 , e, ρ, and σ , if
45. Proof of validity of desugaring for let*. Section 2.13.1 claims that let* can be
desugared as follows:
△
(let* () e) = e
△
(let* ([x1 e1 ] · · · [xn en ]) e) =
(let ([x1 e1 ]) (let* ([x2 e2 ] · · · [xn en ]) e))
Using the same technique as in Exercise 44, prove that these rules are a good
desugaring of let* into let.
DEFıNEOLDGLOBAL
x ∈ dom ρ
he, ρ, σi ⇓ hv, σ ′ i
hd, ρ, σi → hρ′ , σ ′ i
2 hVAL(x, e), ρ, σi → hρ, σ ′ {ρ(x) 7→ v}i
DEFıNENEWGLOBAL
Scheme, x 6∈ dom ρ ℓ 6∈ dom σ
Sexpressions, and he, ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}i ⇓ hv, σ ′ i
firstclass functions hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ {ℓ 7→ v}i
194 DEFıNEFUNCTıON
hVAL(f, LAMBDA(hx1 , . . . , xn i, e)), ρ, σi → hρ′ , σ ′ i
hDEFıNE(f, hx1 , . . . , xn i, e), ρ, σi → hρ′ , σ ′ i
EVALEXP
hVAL(it, e), ρ, σi → hρ′ , σ ′ i
hEXP(e), ρ, σi → hρ′ , σ ′ i
2.16.14 Semantics and design: Using the operational semantics to explore lan
guage design
46. Alternate semantics for val. In both Scheme and µScheme, when val’s left‐
hand side is already bound, val behaves like set. If val instead always cre‐
ated a new binding, the semantics would be simpler.
for a previously undefined u. The definition is valid, and u has a value, al‐
though the value is not specified. Similar behavior is typical of a number of
dynamically typed languages, such as Awk, Icon, and Perl, in which a new
variable—with a well‐specified value, even—can be called into existence just
by referring to it.
In µScheme, the DEFıNENEWGLOBAL rule makes it easy to define recursive
functions, as explained on page 151. But you might prefer a semantics in
which whenever u ∈ / dom ρ, (val u u) is rejected.
himaginary transcript 194bi≡
‑> (val u u)
Run‑time error: variable u not found
49. Operational semantics for shortcircuit &&. Section 2.13.3 proposes syntactic §2.16
sugar for short‐circuit conditionals. To know if the syntactic sugar is any Exercises
good, we have to have a semantics in mind. Use rules of operational seman‐ 195
tics to specify how && should behave. That is, pretending that binary short‐
circuit && is actual abstract syntax and that (&& e1 e2 ) is a valid expression
of µScheme, write rules for the evaluation of && expressions.
2.16.15 Metatheory
51. Proof that variables don’t alias. Use the operational semantics to prove that
variables in µScheme cannot alias. That is, prove that the evaluation of a
µScheme program never constructs an environment ρ such that x and y are
both defined in ρ, x =6 y , and ρ(x) = ρ(y).
Hint: It will help to prove that any location in the range of ρ is also in the
domain of σ .
53. Syntactic sugar for shortcircuit conditionals. In full Scheme, and and or are
macros that behave like the variadic && and || operators defined in Sec‐
tion 2.13.3.
(and) ≡ #t
(and p) ≡p
(and p1 p2 . . . pn ) ≡ (if p1 (and p2 . . . pn ) #f)
Implementing or requires hygiene: you must find a fresh variable x that does
not appear in any ei .
(or) ≡ #f
(or e) ≡e
To develop your understanding, and also to test your work, add two more
parts:
54. Syntactic sugar for records. Implement the syntactic sugar for record de‐
scribed in Section 2.13.6, according to these rules:
△
(record r (f1 · · · fn )) =
(define make‑r (x1 · · · xn )
(cons 'make‑r (cons x1 (cons · · · (cons xn '())))))
(define r ? (x) (&& (pair? x) (= (car x) 'make‑r ) · · · ))
(define r ‑f1 (x) (if (r ? x) (car (cdr x)) (error · · · )))
(define r ‑f2 (x) (if (r ? x) (car (cdr (cdr x))) (error · · · )))
..
.
(define r ‑fn (x) (if (r ? x)
(car (cdr (cdr · · · (cdr x))))
(error · · · )))
This exercise requires a lot of code. To organize it, I use these tricks:
• The record definition desugars into a list of definitions. It’s not shown
in the chapter, but µScheme has a hidden, internal mkDefs function
that turns a list of definitions into a single definition. I build the list of
definitions like this:
hfunctions for desugaring record definitions 196i≡
Deflist desugarRecord(Name recname, Namelist fieldnames) {
return mkDL(recordConstructor(recname, fieldnames),
mkDL(recordPredicate(recname, fieldnames),
recordAccessors(recname, 0, fieldnames)));
}
• I build syntax for calls to the primitives cons, car, cdr, and pair?.
For each of these µScheme primitives, I define a C function, and it calls
the literal primitive directly, like this:
hfunctions for desugaring record definitions 196i+≡
static Exp carexp(Exp e) {
return mkApply(mkLiteral(mkPrimitive(CAR, unary)), mkEL(e, NULL));
}
• I define an auxiliary C function that generates µScheme code that ap‐ §2.16
plies cdr to a list a given number of times. Exercises
• My C code builds syntax for a constructor function, a type predicate, 197
and accessor functions. For each kind of function, I first build an ex‐
pression that represents the body of the function, which I put in a local
variable called body. I then use body in the Def that I return.
55. Quasiquotation. In Section 2.7.1, I put counter operations into a record. An al‐
ternative is to put them into an association list:
htranscript 95ai+≡
‑> (val resettable‑counter‑from
(lambda (x) ; create a counter
(list2
(list2 'step (lambda () (set x (+ x 1))))
(list2 'reset (lambda () (set x 0))))))
The quasiquote form works like the quote form—which is normally written
using the tick mark '—except that it recognizes unquote, and the unquoted
expression is evaluated.
It might not be obvious that using quasiquote and unquote is any nicer than
calling list2 (or full Scheme’s list). But full Scheme provides nice abbre‐
viations: just as quote is normally written with a tick mark, quasiquote and
unquote are normally written with a backtick and a comma, respectively:
hfantasy transcript 197ci+≡
‑> (val resettable‑counter‑from
(lambda (x) ; create a counter
`((step ,(lambda () (set x (+ x 1))))
(reset ,(lambda () (set x 0))))))
list2 B 96
(a) Look at the implementation of quote in Section L.5.1 (page S323), which
relies on functions sSexp and parsesx on page S325. Emulating that
code, write new functions sQuasi and parsequasi and use them to im‐
plement quasiquote and unquote.
(b) Look at the implementation of getpar_in_context in chunk S170. Ex‐
tend the function so that when read_tick_as_quote is set, it not only
reads ' as quote but also reads ` as quasiquote and , as unquote.
2.16.17 Implementing new primitives
56. List construction. Add the new primitive list, which should accept any num‐
ber of arguments.
2 57. Application to a list of arguments constructed dynamically. Add the new prim‐
itive apply, which takes as arguments a function and a list of values, and
Scheme, returns the results of applying the function to the values:
Sexpressions, and
he, ρ, σ0 i ⇓ hPRıMıTıVE(apply), σ1 i
firstclass functions
he1 , ρ, σ1 i ⇓ hv, σ2 i
198 he2 , ρ, σ2 i ⇓ hPAıR(ℓ1 , PAıR(ℓ2 , . . . , PAıR(ℓn , ℓ))), σ3 i
σ3 (ℓ) = NıL σ3 (ℓi ) = vi , 1 ≤ i ≤ n
hAPPLY(LıTERAL(v), LıTERAL(v1 ), LıTERAL(v2 ), …, LıTERAL(vn )), ρ, σ3 i ⇓ hv ′ , σ4 i .
hAPPLY(e, e1 , e2 ), ρ, σ0 i ⇓ hv ′ , σ4 i
(APPLY‐Aſ‐PRıMıTıVE)
Use the read primitive to build an interactive version of the metacircular in‐
terpreter in the Supplement.
60. Call tracing. Instrument the interpreter so it traces calls and returns. When‐
ever a traced function is called, print its name (if any) and arguments. (If the
name of a function is not known, print a representation of its abstract syn‐
tax.) When a traced function returns, print the function and its result.
To help users match calls with returns, indent each call and return should
by an amount proportional to the number of pending calls not yet returned.
Choose one of the following two methods to indicate which functions to
trace:
Test your work by tracing length as shown on page 99. Also trace sieve and
remove‑multiples.
Printing the abstract syntax for a function provides good intuition, but it may
be more helpful to print non‐global functions in closure form.
(c) When calling a closure, print it in closure form instead of printing its
name. Which of the two methods is better?
§2.16
Exercises
199
CHAPTER 3 CONTENTſ
3.1 THE µSCHEME+ LANGUAGE 202 3.6.7 Interpreting forms that
push a single frame 230
3.2 PROCEDURAL
3.6.8 Updating lists of
PROGRAMMıNG WıTH
expressions within frames 231
CONTROL OPERATORſ 205
3.6.9 Interpreting forms that
3.2.1 Programming with break, evaluate expressions in
continue, and return 205 sequence 232
3.2.2 Programming with 3.6.10 Interpreting control
try‑catch and throw 207 operators 236
3.3 OPERATıONAL ſEMANTıCſ: 3.6.11 Implementing proper
EVALUATıON UſıNG A ſTACĸ 210 tail calls 238
GOTO targetlabel
or
But a program full of gotos can be hard to understand: in particular, the order in
which the parts are executed need not have anything to do with the order in which
they are written. The goto statement was eventually derided as “harmful” (Dijkstra
1968), and goto was largely replaced with constructs like if and while. Using if
and while, the order in which the parts are executed is determined by the order
in which they are written, and it’s easy to see. The use of if and while as primary
control‐flow constructs is called structured programming. Structured programming
was the most successful programming‐language revolution of all time—in today’s
languages, if and while are ubiquitous, and despite Donald Ervin Knuth’s (1974)
201
attempts to rehabilitate the goto statement, goto is frowned upon and is almost
never used.
But Knuth had a point. Using if and while exclusively can lead to convoluted
code, especially in loops. To express loops more clearly, structured programming
3 languages adapted:
The action “go to a target” is exactly what is hard to implement in a recursive inter‐
preter and impossible to describe using a judgment of the form he, ρ, σi ⇓ hv, σ ′ i.
Control operators are the subject of this chapter. The chapter presents not only
break, continue, and return, but also try‑catch and throw, which model excep‐
tional control flow between functions; and long‑label and long‑goto, which are
low‐level control operators that can implement all the others. These operators are
specified and implemented using a new technique: an explicit representation of
the context in which each expression is evaluated. Our representation is a stack.
The stack is related to the C call stack of the recursive eval functions in Chapters
1 and 2. It is also related to the path from an evaluation judgment to the root of
a derivation. In our C code, the stack is a data structure, and in our operational
semantics, the stack is a part of the state of the abstract machine.
Most chapters in this book are oriented toward what you can do with new lan‐
guage ideas or new language features. But as you might guess from the talk about a
stack, interpreters, and semantics, this chapter is oriented more toward how con‐
trol operators can be specified and implemented. And in Chapter 4, the implemen‐
tation is extended to show how civilized programming languages manage memory.
If I’m a functional programmer, I might think about the problem like this:
• Typical functional codes manipulate whole data structures: for the rainfall
problem, whole lists. I care about lists like “everything up to 99999” and “all
the nonnegative elements.”
• I might not have to write a new recursive function—maybe I can use an ex‐
isting one. Existing higher‐order functions implement common recursions,
and they’re easy to reuse and combine. For the rainfall problem, takewhile
can grab list elements up to 99999, and filter can grab the nonnegative el‐
ements. Functions like takewhile and filter make it easy to write working
code quickly, although they also make it less obvious how much computer
time and memory are needed.
• Functions takewhile and filter might allocate cons cells, and that’s OK;
a serious implementation of a functional language is designed for programs
that allocate like crazy.
My design plan looks like this: grab all the numbers up to (but not including) 99999,
eliminate the negative ones, and return the sum of what’s left, divided by its length.
“Grab up to” is takewhile and “eliminate” can be done using filter.
206a. htranscript 206ai≡ 206b ▷
‑> (define rainfall‑f (ns)
(let* ([nonneg? (lambda (n) (>= n 0))]
The code is good enough, but if the input list doesn’t contain any nonnegative
numbers, I prefer a different error message. To avoid dividing by (length ms) when
it might be zero, I first check if ms is empty. If so, I issue my own error message.
My revised function looks like this:
206b. htranscript 206ai+≡ ◁ 206a 207a ▷
‑> (define rainfall‑f (ns)
(let* ([nonneg? (lambda (n) (>= n 0))]
[ms (filter nonneg? (takewhile ((curry !=) 99999) ns))])
(if (null? ms)
(error 'rainfall‑no‑nonnegative‑numbers)
(/ (foldl + 0 ms) (length ms)))))
‑> (rainfall‑f '(1 2 3))
2
‑> (rainfall‑f '(99999 1 2 3))
Run‑time error: rainfall‑no‑nonnegative‑numbers
If I’m a procedural programmer, I might think about the problem like this:
• Side effects are OK. The rainfall problem calls for an average, and just as in
the functional solution, I’ll need a total and a count. But in the procedural
solution, the total and count can be kept in mutable variables, which can be
initialized to zero and updated using set.
• Procedural code usually avoids allocation, and for good reason: implementa‐
tions of procedural languages often assume that allocation is rare. The rain‐
fall problem consumes a list that is allocated on the heap, but its total and
count are only numbers, and it returns a number. So it has no reason to al‐
locate.
The challenge of the rainfall problem is what to do in the loop. The loop demands
a nontrivial case analysis: the sentinel value 99999, if present, marks the end of
the input, and other negative values should be ignored. These special cases can be
managed using break and continue. My design plan looks like this:
Control operators break and continue simplify the code significantly (see Exer‐
cise 2), as does the syntactic sugar. And the code works as it should:
207b. htranscript 206ai+≡ ◁ 207a 208a ▷
‑> (rainfall‑p '(1 2 3 99999 4 5 6))
2
‑> (rainfall‑p '(1 ‑1 2 ‑2 3 ‑3 6))
3
‑> (rainfall‑p '(‑1 ‑2 ‑3))
Run‑time error: rainfall‑no‑nonnegative‑numbers
• When the bad or unexpected thing happens, the exception is thrown. (Some
languages say raised or signaled.)
• When an exception is thrown, control is transferred to a handler; the han‐
dler catches the exception. And unlike break, continue, or return, throwing
an exception can transfer control to a distant function: if there’s no handler
in the function where the exception is thrown, the system looks for a handler
3 in the calling function, and then the function that called the calling function,
and so on. The system interrogates older and older active functions until it
finds one that has an appropriate handler.
Control operators
and a smallstep What marks a handler as appropriate depends on the language. In some lan‐
semantics: µScheme+ guages, exceptions have names, and a handler is appropriate if it names the
exception that is thrown. In other languages, exceptions are values, a han‐
208 dler names a type, and a handler is appropriate if the value thrown is com‐
patible with the named type. And in many languages, a handler can claim to
be appropriate for all exceptions.
To implement exceptions, a programming language needs three mechanisms:
After this change, calling a rainfall function without any rainfalls produces a dif‐
ferent error message.
208b. htranscript 206ai+≡ ◁ 208a 209a ▷
‑> (rainfall‑p '())
Run‑time error: long‑goto :error with no active long‑label for :error
This message mentions long‑goto and long‑label because these are the mecha‐
nisms used to implement throw and try‑catch (Section 3.4); the message means
there’s no handler for the :error exception.
A handler can be provided using try‑catch. As a vastly oversimplified exam‐
ple, I define a prediction function. It predicts tomorrow’s rainfall by using the aver‐
1
By convention, I identify each exception using a name that begins with a colon, but you can name
an exception anything you like.
age from rainfall‑p, but if rainfall‑p fails, it predicts a rainfall of zero. Function
predicted‑rainfall works even on inputs where rainfall‑p fails.
209a. htranscript 206ai+≡ ◁ 208b 209b ▷
‑> (define predicted‑rainfall (data)
(try‑catch
(rainfall‑p data) ; this is evaluated in the scope of the handler
§3.2
:error ; this is the exception that the handler catches
(lambda (_) 0) ; this is the handler
Procedural
)) programming with
‑> (predicted‑rainfall '(1 ‑1 2 ‑2 0 99999 6 ‑6)) control operators
1
209
‑> (predicted‑rainfall '(99999 1 ‑1 2 ‑2 0 99999 6 ‑6))
0
‑> (rainfall‑p '(99999 1 ‑1 2 ‑2 0 99999 6 ‑6))
Run‑time error: long‑goto :error with no active long‑label for :error
3 :not‑found
(lambda (x) (list2 'not‑found x)))
(not‑found X)
Control operators
To implement find‑c using the control operator, we install an exception han‐
and a smallstep
dler that invokes the failure continuation.
semantics: µScheme+
210b. htranscript 206ai+≡ ◁ 210a 215 ▷
210 ‑> (define alternate‑find‑c (k alist success‑cont failure‑cont)
(try‑catch (success‑cont (find‑or‑throw k alist))
:not‑found
(lambda (key) (failure‑cont))))
‑> (alternate‑find‑c 'Hello '((Hello Dolly) (Goodnight Irene))
(lambda (v) (list2 'the‑answer‑is v))
(lambda () 'the‑key‑was‑not‑found))
(the‑answer‑is Dolly)
‑> (alternate‑find‑c 'Goodbye '((Hello Dolly) (Goodnight Irene))
(lambda (v) (list2 'the‑answer‑is v))
(lambda () 'the‑key‑was‑not‑found))
the‑key‑was‑not‑found
Continuations also turn out to be a fine way to specify the behavior of control
operators (Stoy 1977; Allison 1986; Schmidt 1986). Sadly, continuation‐based spec‐
ification techniques are beyond the scope of this book.
The state’s first element, which is either e or v , is the current item. And an op‐
erational semantics based on transitions between states like these is an abstract
machine semantics.
Below, the abstract‐machine transitions are illustrated by comparing a stack‐
based evaluation, which uses this machine, with a recursive evaluation, which
gradually fills in a derivation in the style of Chapters 1 and 2. Both evaluations
use the expression
(* (+ 10 1) 9).
The recursive eval from Chapter 2 traverses a derivation tree node by node. In a
template, a partially traversed tree is indicated by the colors of the big‐step judg‐
ments: a judgment whose subderivation is completely traversed is colored black,
and a judgment whose subderivation is not yet traversed is colored gray. A judg‐
ment whose traversal by Chapter 2 eval is currently in progress is shown half and
2
The eval function isn’t special: any recursive function can be converted into a loop that uses an
explicit stack. If you’ve seen this technique before, some of what’s in this section will be old news,
and you can concentrate on what’s happening in the different kinds of semantics. If you haven’t seen
the technique before, be aware that it’s good for more than just semantics: it’s good for any recursive
algorithm that needs very deep recursions. One of my favorite examples is depth‐first search of a graph
with millions of nodes.
half: black on the left and gray on the right. The colors can help you compare the
progress of the two evaluations: if you follow a path from the uppermost call in
progress down to the root of the derivation tree, you’ll see that each judgment on
the path corresponds to a frame on the stack.
3 In the stack‐based evaluation, each state in which the current item is an expres‐
sion is labeled EXP, and each state in which the current item is a value is labeled
VALUE. And to make it extra easy to distinguish values from literal expressions, in
Control operators
this example only, values are written using an italic font, as in 99.
and a smallstep
Recursive evaluation starts by passing expression e = (* (+ 10 1) 9) to eval.
semantics: µScheme+
Stack‐based evaluation starts in a state in which the current item is the expression e
212 and the stack is empty:
After 9 steps, the current item is a value and the stack is empty, so h99, ρ, σ, []i is a
valid state, and the result of evaluating the expression is 99.
stack frame—and there are about as many forms of stack frame as there are forms
of expression. The drawback can be mitigated by reducing the number of forms of
expression. To do that, some forms of expression are lowered to a core language.
A core language is a subset that is sufficient to express everything in a full lan‐
guage. The full language is lowered to a core language by a process of rewriting
expressions. It works much the same way as expanding syntactic sugar, except
when a full language is lowered to a core language, the original syntax is usually
kept around to be used in error messages.
Core µScheme+ includes long‑label and long‑goto forms, but not break,
continue, try‑catch, or throw forms—these forms are lowered into the core. Core
µScheme+ also omits begin and let* forms, which are lowered to let expressions.
Finally, Core µScheme+ does include return; while return can be lowered using
long‑goto, lowering return would complicate the important tail‐call optimization
described on page 238. Lowering return is left as Exercise 23.
Expressions are lowered using the rules in Table 3.4. Each rule is written as
a relation of the form e ⇝ e′ , pronounced “expression e is lowered to expres‐
sion e′ .” Rules in the first two groups use long‑label and long‑goto to imple‐
ment µScheme+’s other control operators. Operators break, continue, throw, and
return can be implemented using long‑goto, for which suitable labels are intro‐
duced by the rules for while, try‑catch, and lambda.
The only rules that demand detailed explanation are those for try‑catch and
throw. The try‑catch rule sets up a long‑label expression whose result is a func‐
tion; that function takes one argument, a handler, and it returns the result of the
try‑catch. When try‑catch terminates as the result of a throw, the function
thrown is (lambda (h) (h x)), where x is the value thrown. This case therefore
passes the value to the handler. When try‑catch terminates as the result of its
body terminating normally, the function produced is (lambda (h) x), where x is
the value of the body. This case ignores the handler.
Table 3.4 omits some important side conditions: break and continue expres‐
sions are lowered only inside a loop, and a return expression is lowered only inside
a function. Attempts to lower these operators outside of their expected contexts re‐
sult in an error message:
§3.5
215. htranscript 206ai+≡ ◁ 210b A semantics of
‑> (lambda (x) (when x (break)))
Core µScheme+
Lowering error: (break) appeared outside of any loop
The lowering transformation reduces the number of rules needed to express 215
the operational semantics: all the forms that can be lowered share a single rule, and
none of those forms ever goes onto the evaluation stack. The operational semantics
itself is the topic of the next section.
The example abstract‐machine transitions in Section 3.3 are justified by the opera‐
tional semantics of Core µScheme+. This semantics is an abstractmachine seman
tics, and it is defined by this transition relation between machine states:
he/v, ρ, σ, Si → he′ /v ′ , ρ′ , σ ′ , S ′ i.
The notation e/v stands for the current item, which may be an expression e or a
value v .
An abstract‐machine transition describes just one step in an evaluation, not
the evaluation of an entire expression. Evaluating an entire expression usually re‐
quires multiple steps. A transition that may take multiple steps (zero or more) is
written
he/v, ρ, σ, Si →∗ he′ /v ′ , ρ′ , σ ′ , S ′ i.
The sequence of states thus passed through is often called a reduction sequence.
Some special states and transitions are worth looking out for; spotting them
will help you understand how the machine works.
• A state of the form hv, ρ, σ, []i is a final state, reached after an evaluation
completes. States of this form are the only acceptable final states; if the ma‐
chine is in any other state and it cannot make a transition, it is considered
stuck.
None of these special cases describes the evaluation of a control operator, which
typically inspects multiple frames on the stack.
The permissible transitions of the abstract machine are described by inference
rules. These rules describe actions that differ from the actions described by big‐
step rules: In a big‐step semantics, the action of evaluating a typical syntactic form
is described by a single rule, and the evaluation of a conditional form, like if or
while, is typically described by two rules: one for a true condition and one for a
false condition. In an abstract‐machine semantics, the action of evaluating any
syntactic form is usually spread out over at least two rules, and sometimes more.
One rule says what to do with the form if it appears as the current item, and one
3 says what to do if the form appears on the stack as a frame. And the rules for stack
frames tend to be less uniform big‐step rules: some forms never appear on the
stack, and others may appear as frames with holes in different places, requiring
Control operators
multiple rules per form.
and a smallstep
Because the abstract‐machine rules work so differently from the big‐step rules,
semantics: µScheme+
they are organized differently from the rules in Chapters 1 and 2. The rules for a
216 single syntactic form still appear together, but based on how their evaluation affects
the stack, the forms are organized into these groups:
This grouping contrasts with all the other groupings in the book, which mostly
show the same constructs in the same order (literal, variable, set, if, while, and
so on).
To help you compare the small‐step reduction rules with big‐step natural‐
deduction rules, rules of µScheme+ are accompanied by corresponding rules of
µScheme. If you want to leap straight to the control operators, they are described
in Section 3.5.5 on page 221.
e ⇝ e′ . (LOWER)
he, ρ, σ, Si → he′ , ρ, σ, Si
The stack, environment, and store aren’t consulted and don’t change.
While the semantics specifies that an expression be lowered only when it is
about to be evaluated, the lowering relation depends only on e’s syntactic form,
not on any other property of the machine’s state. This independence enables an
optimization: all lowerable expressions can be lowered preemptively, before eval‐
uation begins. In my implementation, no expression is lowered more than once,
even if it is evaluated in a loop.
A literal is evaluated without looking at the stack or the store. The small‐step rule
looks almost exactly like the big‐step rule.
(BıG‐STEP‐LıTERAL)
hLıTERAL(v), ρ, σi ⇓ hv, σi
(SMALL‐STEP‐LıTERAL)
hLıTERAL(v), ρ, σ, Si → hv, ρ, σ, Si
Likewise, a variable is evaluated in one small step. The lookup is the same as
in the big‐step rule, and the stack S is unexamined and unchanged.
x1 , . . . , xn all distinct
hLAMBDA(hx1 , . . . , xn i, e), ρ, σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, e), ρ |), σi
(BıG‐STEP‐MĸCLOſURE)
x1 , . . . , xn all distinct
hLAMBDA(hx1 , . . . , xn i, e), ρ, σ, Si → h(|LAMBDA(hx1 , . . . , xn i, e), ρ |), ρ, σ, Si
(SMALL‐STEP‐MĸCLOſURE)
x ∈ dom ρ . (SMALL‐STEP‐AſſıGN)
hſET(x, e), ρ, σ, Si → he, ρ, σ, ſET(x, •) :: Si
When e’s evaluation is complete, its value v will be the current item, and the frame
ſET(x, •) will be on top of the stack. The abstract machine must pop the stack,
update location ℓ = ρ(x), and produce v .
ρ(x) = ℓ
(FıNıſH‐AſſıGN)
hv, ρ, σ, ſET(x, •) :: Si → hv, ρ, σ{ℓ 7→ v}, Si
3 hıF(e1 , e2 , e3 ), ρ, σi ⇓ hv2 , σ ′′ i
(BıG‐STEP‐IFTRUE)
Control operators
and a smallstep
semantics: µScheme+ he1 , ρ, σi ⇓ hv1 , σ ′ i v1 = BOOLV(#f) he3 , ρ, σ ′ i ⇓ hv3 , σ ′′ i
hıF(e1 , e2 , e3 ), ρ, σi ⇓ hv3 , σ ′′ i
218 (BıG‐STEP‐IFFALſE)
Look at both rules above the line. Each one begins with the same judgment,
he1 , ρ, σi ⇓ hv1 , σ ′ i. Therefore, the small‐step semantics can begin with the eval‐
uation of e1 , and it can push a frame that waits for value v1 . Again, that frame is
made by replacing e1 with a hole. The frame ıF(•, e2 , e3 ) saves both e2 and e3 on
the stack, so no matter what v1 is, the machine knows how to continue:
. (SMALL‐STEP‐IF)
hıF(e1 , e2 , e3 ), ρ, σ, Si → he1 , ρ, σ, ıF(•, e2 , e3 ) :: Si
When ıF(•, e2 , e3 ) is on the stack and v is the current item, the machine continues
by evaluating either e2 or e3 —whichever would be dictated by the big‐step rules:
v 6= BOOLV(#f) , (SMALL‐STEP‐IF‐TRUE)
hv, ρ, σ, ıF(•, e2 , e3 ) :: Si → he2 , ρ, σ, Si
v = BOOLV(#f) . (SMALL‐STEP‐IF‐FALſE)
hv, ρ, σ, ıF(•, e2 , e3 ) :: Si → he3 , ρ, σ, Si
The forms that evaluate sequences of expressions (APPLY, LET, and LETREC) use
the same idea as ſET and ıF—push a frame where a hole marks what the machine
is waiting for—but because values in the sequence are delivered one at a time, the
rules are more complicated. Each of these forms uses at least three kinds of rule:
• Every form has a rule for when the form is encountered as the current item.
That rule turns the form into a frame by putting a hole in the first position.
• Every form has a rule for a hole in the middle of the sequence. The rule fills
the hole with a value and moves the hole to the next position in sequence.
• Every form has a rule for a hole in the last position in the sequence. That rule
captures the entire sequence and continues.
In addition, the APPLY form has a special rule for a hole in the first position, because
that’s the function position, not an argument position, and the function is treated
specially.
Function application
Function application has a lot going on. Here’s the big‐step rule:
.
hv, ρ, σ, APPLY(vf , v1 , . . . , vi−1 , •, ei+1 , . . . , en ) :: Si →
hei+1 , ρ, σ, APPLY(vf , v1 , . . . , vi−1 , v, •, ei+2 , . . . , en ) :: Si
(SMALL‐STEP‐APPLY‐NEXT‐ARG)
This rule corresponds to the transition that finishes the right‐hand side of big‐step
judgment hei , ρ, σi−1 i ⇓ hvi , σi i (where v = vi ) and starts the next judgment
hei+1 , ρ, σi i ⇓ hvi+1 , σi+1 i. If the hole is in the first (function) position, a very
similar rule applies:
.
hv, ρ, σ, APPLY(•, e1 , . . . , en ) :: Si → he1 , ρ, σ, APPLY(v, •, e2 , . . . , en ) :: Si
(SMALL‐STEP‐APPLY‐FıRſT‐ARG)
The APPLY(v, e1 , . . . , en ) frame is first pushed onto the stack when a function appli‐
cation appears as the current expression. The expression e in the function position
becomes the new current item, i.e., the next thing to be evaluated.
vf = (|LAMBDA(hx1 , . . . , xn i, ec ), ρc |)
3 ℓ1 , . . . , ℓn ∈
/ dom σ (and all distinct)
hvn , ρ, σ, APPLY(vf , v1 , . . . , vn−1 , •) :: Si →
Control operators hec , ρc {x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }, ENV(ρ, CALL) :: Si
and a smallstep (SMALL‐STEP‐APPLY‐LAſT‐ARG)
semantics: µScheme+ After ec ’s evaluation is finished, encountering ENV(ρ, CALL) on the stack restores ρ:
.
220 hv, ρ′ , σ, ENV(ρ, tag) :: Si → hv, ρ, σ, Si
(SMALL‐STEP‐REſTORE‐ENVıRONMENT)
In informal English, the environment is pushed onto the stack just before a call,
and when the call finishes, it is popped back off. This part of the semantics models
real implementations of real languages: environment ρ holds machine registers
and local variables, and ENV(ρ, CALL) is called a “stack frame” (or sometimes “ac‐
tivation record.”).
Rule SMALL‐STEP‐APPLY‐LAſT‐ARG applies only when vf is a closure. When vf
is a primitive function, other rules apply, like this one:
Sequences of expressions also occur in LET forms. In µScheme+, only LET and
LETREC are part of the core; LETſTAR is lowered. Like APPLY, LET evaluates ex‐
pressions in sequence and builds a new environment in which to evaluate a body.
,
hv, ρ, σ, LET(hx1 , v1 , . . . , xi , •, xi+1 , ei+1 , . . . , xn , en i, e) :: Si →
hei+1 , ρ, σ, LET(hx1 , v1 , . . . , xi , v, xi+1 , •, . . . , en i, e) :: Si
(SMALL‐STEP‐NEXT‐LET‐EXP)
x1 , . . . , xn all distinct , (SMALL‐STEP‐LET)
hLET(hx1 , e1 , . . . , xn , en i, e), ρ, σ, Si →
he1 , ρ, σ, LET(hx1 , •, x2 , e2 , . . . , xn , en i, e) :: Si
ℓ1 , . . . , ℓn ∈
/ dom σ (and all distinct) ,
hv, ρ, σ, LET(hx1 , v1 , . . . , xn , •i, e) :: Si →
he, ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }, ENV(ρ, NONCALL) :: Si
(SMALL‐STEP‐LET‐BODY)
.
hv, ρ′ , σ, ENV(ρ, NONCALL) :: Si → hv, ρ, σ, Si
(SMALL‐STEP‐REſTORE‐LET‐ENVıRONMENT)
The frame ENV(ρ, NONCALL) behaves the same way as ENV(ρ, CALL)—except, as
shown on page 222, when viewed by the control operator return.
The LETREC expression is quite similar to the LET expression, except it binds
fresh locations into the environment before evaluating expressions e1 , . . . , en .
x1 , . . . , xn all distinct
ei has the form LAMBDA(· · ·), 1 ≤ i ≤ n
ℓ1 , . . . , ℓn ∈ / dom σ (and all distinct)
ρ′ = ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }
σ0 = σ{ℓ1 7→ unspecified, . . . , ℓn 7→ unspecified} ,
hLETREC(hx1 , e1 , . . . , xn , en i, e), ρ, σ, Si →
he1 , ρ′ , σ0 , LETREC(hx1 , •, x2 , e2 , . . . , xn , en i, e) :: ENV(ρ, NONCALL) :: Si
(SMALL‐STEP‐LETREC)
ℓi = ρ(xi ), 1 ≤ i ≤ n .
hv, ρ, σ, LETREC(hx1 , v1 , . . . , xn , •i, e) :: Si →
he, ρ, σ{ℓ1 7→ v1 , . . . , ℓn−1 7→ vn−1 , ℓn 7→ v}, Si
(SMALL‐STEP‐LETREC‐BODY)
3.5.5 Forms that inspect the stack: LONG‐LABEL, LONG‐GOTO, and RETURN
Finally, the control operators! These operators can’t easily be described using big‐
step semantics. Each operator carries one expression, and each operator begins
by pushing itself on the stack with a hole in place of its expression. The expression
becomes the current item, and when it is reduced to a value, the fun begins:
1. If LONG‐LABEL(L, •) is on top of the stack, it doesn’t do anything—it’s there
just to mark a destination for LONG‐GOTO. The frame is popped and evalua‐
tion continues.
3 A LONG‐LABEL expression pushes its label onto the stack and evaluates its body.
It also saves the current environment just below the label, so that after a control
transfer, the environment is properly restored.
Control operators
and a smallstep hLONG‐LABEL(L, e), ρ, σ, Si → he, ρ, σ, LONG‐LABEL(L, •) :: ENV(ρ, NONCALL) :: Si
semantics: µScheme+ (LABEL)
If control is never transferred, eventually the LONG‐LABEL frame is found on top of
222 the stack, where it is ignored.
(LABEL‐UNUſED)
hv, ρ, σ, LONG‐LABEL(L, •) :: Si → hv, ρ, σ, Si
The LONG‐LABEL frame is actually used as a target for LONG‐GOTO, which be‐
gins by evaluating its expression:
. (GOTO)
hLONG‐GOTO(L, e), ρ, σ, Si → he, ρ, σ, LONG‐GOTO(L, •) :: Si
Once the expression is evaluated, the LONG‐GOTO continues by looking for its
matching label.
F 6= LONG‐LABEL(L, •) .
hv, ρ, σ, LONG‐GOTO(L, •) :: F :: Si → hv, ρ, σ, LONG‐GOTO(L, •) :: Si
(GOTO‐UNWıND)
A RETURN works like a LONG‐GOTO, except instead of looking for a correspond‐
ing LONG‐LABEL, it looks for a frame of the form ENV(ρ′ , CALL).
(RETURN)
hRETURN(e), ρ, σ, Si → he, ρ, σ, RETURN(•) :: Si
(RETURN‐TRANſFER)
hv, ρ, σ, RETURN(•) :: ENV(ρ′ , CALL) :: Si → hv, ρ′ , σ, Si
F does not have the form ENV(ρ′ , CALL)
(RETURN‐UNWıND)
hv, ρ, σ, RETURN(•) :: F :: Si → hv, ρ, σ, RETURN(•) :: Si
As in Chapter 2, the judgment form hd, ρ, σi → hρ′ , σ ′ i says that the result of
evaluating definition d in environment ρ with store σ is a new environment ρ′ and
a new store σ ′ . This judgment is a big‐step judgment, just as in Chapter 2. And as
in Chapter 2, DEFıNE is syntactic sugar for a VAL binding to a LAMBDA expression,
and a top‐level expression is syntactic sugar for a binding to the global variable it,
so the rules for DEFıNE and EXP are the same as in Chapter 2. But because the
evaluation judgment for expressions is different, the rules for evaluating VAL bind‐
ings are also different: they use the small‐step evaluation relation presented in this
chapter.
A VAL form is treated differently depending on whether its variable is already
bound in the environment.
x ∈ dom ρ
he, ρ, σi ⇓ hv, σ ′ i
(BıG‐STEP‐DEFıNEOLDGLOBAL)
hVAL(x, e), ρ, σi → hρ, σ ′ {ρ(x) 7→ v}i
x 6∈ dom ρ ℓ 6∈ dom σ
§3.6
hſET(x, e), ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}i ⇓ hv, σ ′ i The interpreter
hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ i
(BıG‐STEP‐DEFıNENEWGLOBAL) 223
The small‐step versions of these rules are nearly identical, except that above the
line, the big‐step judgment he, ρ, σi ⇓ hv, σ ′ i is replaced by the transitive closure of
the small step evaluation relation: he, ρ, σ, []i →∗ hv, ρ′ , σ ′ , []i. The final value v
and store σ ′ are used below the line, and the final environment ρ′ is thrown away.
(It is a metatheorem of this semantics that ρ′ is the same as ρ.)
x ∈ dom ρ
he, ρ, σ, []i →∗ hv, ρ, σ ′ , []i
(SMALL‐STEP‐DEFıNEOLDGLOBAL)
hVAL(x, e), ρ, σi → hρ, σ ′ {ρ(x) 7→ v}i
x 6∈ dom ρ ℓ 6∈ dom σ
hſET(x, e), ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}, []i →∗ hv, ρ, σ ′ , []i
hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ i
(SMALL‐STEP‐DEFıNENEWGLOBAL)
The evaluator for µScheme+, which uses the stack described in the operational
semantics, is presented below. The stack itself is a standard data structure, so its
implementation is relegated to Appendix M, as are functions for debugging, mem‐
ory management, and parsing control operators. The rest of the interpreter, in‐
cluding the µScheme parser, initial basis, primitives, and so on, is shared with the
µScheme interpreter described in Chapter 2.
Of all the interpreters in this book, the µScheme+ interpreter least resembles a
real‐life interpreter. The stack inspection for long‑goto is good, and using a stack
to help evaluate expressions is common, but using a stack to implement break and
continue is bizarre; sensible implementors use a stack to manage control transfers
between procedures, not within a single procedure.
A frame may hold a saved environment, and such frames are pushed by the
special function pushenv_opt, which can optimize tail calls (Section 3.6.11).
224d. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 224c 225a ▷
void pushenv_opt(Env env, SavedEnvTag tag, Stack s); // may optimize
Instrumentation
To help you understand what happens on the evaluation stack, the µScheme+ in‐
terpreter is instrumented with three options. Each option is a µScheme+ variable
whose value can influence the behavior of the interpreter.
The µScheme+ interpreter is used not only in this chapter but also in Chapter 4,
which focuses on garbage collection. To help debug the garbage collectors, the
interpreter frequently calls the validate function; provided the argument v repre‐
sents a valid value, validate(v) returns v.
225c. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 225b 231b ▷
Value validate(Value v);
Expressions are divided into four groups. The first group contains the forms
that are found in µScheme:
225e. hast.t 225di+≡ ◁ 225d 225f ▷
Exp* = LITERAL (Value) type Env 153a
| VAR (Name) type Exp A
| SET (Name name, Exp exp) type Frame 223
| IFX (Exp cond, Exp truex, Exp falsex) type Name 43a
type SavedEnvTag
| WHILEX (Exp cond, Exp body)
226b
| BEGIN (Explist) type Stack 223
| LETX (Letkeyword let, Namelist xs, Explist es, Exp body) type Value A
| LAMBDAX (Lambda)
| APPLY (Exp fn, Explist actuals)
The second group contains the new forms, all of which relate to control:
225f. hast.t 225di+≡ ◁ 225e 226a ▷
| BREAKX
| CONTINUEX
| RETURNX (Exp)
| THROW (Name label, Exp exp)
| TRY_CATCH (Exp body, Name label, Exp handler)
| LONG_LABEL (Name label, Exp body)
| LONG_GOTO (Name label, Exp exp)
The third group contains two forms that are used only in frames: the saved‐
environment frame and the hole.
226a. hast.t 225di+≡ ◁ 225f 226c ▷
| ENV (Env contents, SavedEnvTag tag)
3 | HOLE
3.6.3 Lowering
The lowering transformation defined in Table 3.4 (page 214) takes an additional
“lowering context” parameter that is not shown in the table. That parameter tells
the transformation whether an expression appears inside a loop, inside the body
of a function, or both. Operators break, continue, and return are lowered only
when they appear in an appropriate context.
226d. htype definitions for µScheme+ 223i+≡ (S358) ◁ 226b
typedef enum { LOOPCONTEXT = 0x01, FUNCONTEXT = 0x02 } LoweringContext;
A lowering function is defined for every kind of syntactic form that can contain
an expression: true definitions, tests, extended definitions, and so on. All these
functions call lower, which lowers an expression. Calling lower(context , e) re‐
cursively lowers every subexpression of e. And if the form of e calls for it to be
lowered, lower returns e’s LOWERED form; otherwise it returns e. The code is repet‐
itive, and it just implements the rules shown in Table 3.4, so only two cases are
shown here. The rest are relegated to Appendix M.
226e. hdefinition of private function lower 226ei≡ (S350g)
static Exp lower(LoweringContext c, Exp e) {
switch (e‑>alt) {
case SET:
e‑>set.exp = lower(c, e‑>set.exp);
return e;
case BREAKX:
if (c & LOOPCONTEXT)
return mkLowered(e, mkLongGoto(strtoname(":break"),
mkLiteral(falsev)));
else
othererror("Lowering error: %e appeared outside of any loop", e);
hother cases for lowering expression e S351fi
}
}
3.6.4 Structure and invariants of the evaluator
• The environment ρ is always in env and the stack is always in evalstack. §3.6
As part of the state transition, these variables are mutated in place to hold ρ′ The interpreter
and S ′ , respectively.
227
• When the stack is not empty, the youngest frame (the “top”) is pointed to by
local variable fr (for “frame”).
• When the current item is a value v , that value is stored in local variable v,
and the state transition begins at label value.
• Each state transition ends with goto exp or goto value. Before the goto, ei‐
ther e or v is set to the current item for the next state. Variables env and
evalstack are also set.
3 case LITERAL:
case VAR:
hstart e‑>literal and step to the next state 229ci
hstart e‑>var and step to the next state 229di
case SET: hstart e‑>set and step to the next state 230ai
Control operators case IFX: hstart e‑>ifx and step to the next state 230ci
and a smallstep case LETX:
semantics: µScheme+ if (he‑>letx contains no bindings 234bi) {
hcontinue by evaluating the body of the let or letrec 234ci
228 } else {
switch (e‑>letx.let) {
case LET: hstart LET e‑>letx and step to the next state 234di
case LETSTAR: goto want_lowered;
case LETREC: hstart LETREC e‑>letx and step to the next state 235ai
default: assert(0);
}
}
case LAMBDAX: hstart e‑>lambdax and step to the next state 229ei
case APPLY: hstart e‑>apply and step to the next state 232ci
case RETURNX: hstart e‑>returnx and step to the next state 237ci
case LONG_LABEL: hstart e‑>long_label and step to the next state 236di
case LONG_GOTO: hstart e‑>long_goto and step to the next state 237ai
case LOWERED: hreplace e with its lowered form and continue in this state 229ai
case LOOPBACK: hlook inside LOOPBACK and continue in this state 229bi
case WHILEX: case BEGIN: case BREAKX: case CONTINUEX:
case THROW: case TRY_CATCH:
want_lowered: runerror("internal error: expression %e not lowered", e);
hexpressionevaluation cases for forms that appear only as frames S357ai
}
An empty LETX form has no place for a hole, so that case is handled separately.
When the current item is a value v , 9 of the 22 expression forms in µScheme+
may legitimately appear as the youngest (top) frame on the stack, fr.
228b. htake a step from state hv, ρ, σ, fr :: Si 228bi≡ (227b)
switch (fr‑>form.alt) {
case SET: hfill hole in fr‑>form.set and step to the next state 230bi
case IFX: hfill hole in fr‑>form.ifx and step to the next state 231ai
case APPLY: hfill hole in fr‑>form.apply and step to the next state 233ai
case LETX:
switch (fr‑>form.letx.let) {
case LET: hcontinue with let frame fr‑>form.letx 235bi
case LETSTAR: goto want_lowered;
case LETREC: hcontinue with letrec frame fr‑>form.letx 236ai
default: assert(0);
}
case ENV: hrestore env from fr‑>form.env, pop the stack, and step 236ci
case RETURNX: hreturn v from the current function (left as exercise)i
case LONG_GOTO: hunwind v to the nearest matching long‑label 237bi
case LONG_LABEL: hpop the stack and step to the next state 236ei
hcases for forms that never appear as frames S357bi
}
The actions in each state are implemented below, starting with the simplest forms—
the ones that don’t look at the stack.
3.6.5 Interpreting forms that don’t change the stack
To evalute an expression that has been lowered, replace it with its lowered form.
Don’t change the stack.
e ⇝ e′
(LOWER)
he, ρ, σ, Si → he′ , ρ, σ, Si
229a. hreplace e with its lowered form and continue in this state 229ai≡ (228a)
e = e‑>lowered.after;
§3.6
goto exp;
The interpreter
To evaluate an expression tagged with LOOPBACK, replace the tagged form with
the untagged form. Don’t change the stack. (The LOOPBACK tag is used only by the 229
garbage collector in Chapter 4.)
229b. hlook inside LOOPBACK and continue in this state 229bi≡ (228a)
e = e‑>loopback;
goto exp;
To evaluate a literal expression, take v out of the expression and make it the
current item. Don’t change the stack.
(SMALL‐STEP‐LıTERAL)
hLıTERAL(v), ρ, σ, Si → hv, ρ, σ, Si
The machine must step to a state of the form hv, ρ, σ, Si. Environment ρ and store
σ are unchanged, so the machine needs only to update v and to go to value.
229c. hstart e‑>literal and step to the next state 229ci≡ (228a)
v = e‑>literal;
goto value;
To evaluate a variable, look up its value and make that the current item. Don’t
change the stack.
229d. hstart e‑>var and step to the next state 229di≡ (228a)
if (find(e‑>var, env) == NULL)
runerror("variable %n not found", e‑>var);
v = *find(e‑>var, env);
goto value;
To evaluate a LAMBDA, allocate a closure and make it the current item. Don’t
change the stack.
env 227a
x1 , . . . , xn all distinct find 153b
fr 227a
hLAMBDA(hx1 , . . . , xn i, e), ρ, σ, Si → h(|LAMBDA(hx1 , . . . , xn i, e), ρ |), ρ, σ, Si mkClosure A
(SMALL‐STEP‐MĸCLOſURE) runerror 47a
Formal parameters x1 , . . . , xn are confirmed to be distinct when e is parsed.
229e. hstart e‑>lambdax and step to the next state 229ei≡ (228a)
v = mkClosure(e‑>lambdax, env);
goto value;
Unlike the rules above, most small‐step rules show transition from one stack to a
different stack. The implementation keeps just one stack, which it updates in place.
Because the stack contains frames, and a frame can be formed from an expression
in the program, frames need to be updated in a way that does not overwrite the
syntax of the program. And frames’ memory needs to be allocated, preferably in a
way that is more efficient than calling malloc before every push.
Both needs are met by the Stack abstraction, which allocates memory for a
3 large block of frames at once. A frame is represented as described by struct Frame
in chunk 224a. Its form field is not a pointer; it is a struct Exp whose memory is
part of the Frame. And pushframe (chunk 224b) pushes a struct Exp, not a pointer
Control operators
to one. So for example, to push a frame like ıF(•, e2 , e3 ), the interpreter builds the
and a smallstep
new frame using mkIfStruct, not mkIf:
semantics: µScheme+
pushframe(mkIfStruct(hole, e2 , e3 ), evalstack).
230
Neither mkIfStruct nor pushframe allocates; this code builds a frame in memory
that is owned by evalstack. So any field of any frame can be overwritten without
affecting the expression from which that frame was built. This technique is all that
is needed to implement ſET and ıF.
To evaluate ſET, push a frame on the stack, then evaluate the right‐hand side.
x ∈ dom ρ
(SMALL‐STEP‐AſſıGN)
hſET(x, e), ρ, σ, Si → he, ρ, σ, ſET(x, •) :: Si
To implement the transition, the machine pushes the frame ſET(x, •), updates e,
and goes to exp. Environment and store are unchanged.
230a. hstart e‑>set and step to the next state 230ai≡ (228a)
if (find(e‑>set.name, env) == NULL)
runerror("set unbound variable %n", e‑>set.name);
pushframe(mkSetStruct(e‑>set.name, hole), evalstack);
e = e‑>set.exp;
goto exp;
The ſET is completed by the FıNıſH‐AſſıGN rule. When the youngest frame on the
stack is a ſET(x, •) frame, the machine completes the ſET by assigning v to x, then
pops the frame.
ρ(x) = ℓ
(FıNıſH‐AſſıGN)
hv, ρ, σ, ſET(x, •) :: Si → hv, ρ, σ{ℓ 7→ v}, Si
230b. hfill hole in fr‑>form.set and step to the next state 230bi≡ (228b)
assert(fr‑>form.set.exp‑>alt == HOLE);
assert(find(fr‑>form.set.name, env) != NULL);
*find(fr‑>form.set.name, env) = validate(v);
popframe(evalstack);
goto value;
(SMALL‐STEP‐IF)
hıF(e1 , e2 , e3 ), ρ, σ, Si → he1 , ρ, σ, ıF(•, e2 , e3 ) :: Si
230c. hstart e‑>ifx and step to the next state 230ci≡ (228a)
pushframe(mkIfxStruct(hole, e‑>ifx.truex, e‑>ifx.falsex), evalstack);
e = e‑>ifx.cond;
goto exp;
When the youngest frame on the stack is an ıF(•, e2 , e3 ) frame, the machine con‐
tinues with e2 or e3 , as determined by v.
v= 6 BOOLV(#f)
(SMALL‐STEP‐IF‐TRUE)
hv, ρ, σ, ıF(•, e2 , e3 ) :: Si → he2 , ρ, σ, Si
v = BOOLV(#f)
(SMALL‐STEP‐IF‐FALſE)
hv, ρ, σ, ıF(•, e2 , e3 ) :: Si → he3 , ρ, σ, Si
231a. hfill hole in fr‑>form.ifx and step to the next state 231ai≡ (228b) §3.6
assert(fr‑>form.ifx.cond‑>alt == HOLE); The interpreter
e = istrue(v) ? fr‑>form.ifx.truex : fr‑>form.ifx.falsex;
popframe(evalstack); 231
goto exp;
Next up is function application, which calls for infrastructure that can update
a list of expressions within a frame.
APPLY and LET expressions evaluate expressions in sequence. Values are accumu‐
lated by repeating a transition like the one in the SMALL‐STEP‐APPLY‐NEXT‐ARG
rule on page 219, which takes a frame like APPLY(vf , v1 , . . . , vi−1 , •, ei+1 , . . . , en )
to one like APPLY(vf , v1 , . . . , vi−1 , v, •, ei+2 , . . . , en ). In the originating frame,
the sequence v1 , . . . , vi−1 , •, ei+1 , . . . , en can be represented as a value of type
Explist, where expressions ei+1 . . . en come from the original syntax, and val‐
ues v1 , . . . , vi−1 are represented as LıTERAL expressions. The transition is made
by function transition_explist, which overwrites the hole with v, writes a new
hole one position to the right, and returns the expression that was overwritten by
the new hole. That expression is stored in static memory, so subsequent calls to
transition_explist overwrite previous results.
231b. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 225c 231c ▷
Exp transition_explist(Explist es, Value v); // pointer to static memory
When the hole is in the rightmost position, transition_explist overwrites the
env 227a
hole with v and then returns NULL.
evalstack 227a
What about initializing a frame by putting a hole in the first position? Function type Exp A
head_replaced_with_hole works much like transition_explist: it puts a hole in type Explist S309b
the initial position and returns a pointer to the expression that was there. If the list find 153b
fr 227a
is empty, so there is no initial position, it returns NULL.
hole S357c
231c. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 231b 231d ▷ istrue 154c
Exp head_replaced_with_hole(Explist es); // shares memory mkIfxStruct A
// with transition_explist mkSetStruct A
popframe 224b
A function like transition_explist helps implement APPLY, LET, and LETREC, pushframe 224b
but it has to be used carefully: if an Explist is overwritten, it can’t be the origi‐ runerror 47a
nal Explist from the syntax—it has to be a copy. The copy should be made when validate 225c
type Value A
the frame containing the Explist is first pushed. For example, when the inter‐
preter pushes a frame like APPLY(•, e1 , . . . , en ), it copies the list of expressions
using function copyEL:
pushframe(mkApplyStruct(mkHole(), copyEL(e1 , . . . , en )), evalstack).
Function copyEL copies a list of expressions, and when the interpreter is finished
with the copy, freeEL recovers the memory.
231d. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 231c 232a ▷
Explist copyEL(Explist es);
void freeEL(Explist es);
The interpreter calls freeEL when popping a frame that contains a copied Explist.
When an Explist appears in an APPLY, LET, or LETREC frame, each element
goes through three states:
1. Initially it points to fresh memory that contains a copy of syntax from the
3 original expression.
2. At some point the syntax is copied into static memory and the element’s own
memory is overwritten to contain a hole.
Control operators
and a smallstep 3. Finally the element’s own memory is overwritten with the value that results
semantics: µScheme+ from evaluating the original expression.
232 Once every element has reached its final state, the Explist contains only literals,
and it can be converted to a list of values:
232a. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 231d 232b ▷
Valuelist asLiterals(Explist es);
Value asLiteral (Exp e);
Function asLiteral implements the same conversion, but for a single Exp. And
because function asLiterals has to allocate, I provide freeVL, which frees the
memory allocated by asLiterals.
232b. hfunction prototypes for µScheme+ 224bi+≡ (S358) ◁ 232a
void freeVL(Valuelist vs);
These tools enable us to interpret forms that evaluate expressions in sequence.
Function application
,
hv, ρ, σ, APPLY(vf , v1 , . . . , vi−1 , •, ei+1 , . . . , en ) :: Si →
hei+1 , ρ, σ, APPLY(vf , v1 , . . . , vi−1 , v, •, ei+2 , . . . , en ) :: Si
(SMALL‐STEP‐APPLY‐NEXT‐ARG)
vf = (|LAMBDA(hx1 , . . . , xn i, ec ), ρc |)
ℓ1 , . . . , ℓn ∈
/ dom σ (and all distinct) .
hvn , ρ, σ, APPLY(vf , v1 , . . . , vn−1 , •) :: Si →
hec , ρc {x1 →
7 ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }, ENV(ρ, CALL) :: Si
(SMALL‐STEP‐APPLY‐LAſT‐ARG)
Which rule does the dictating depends how the hole appears: as the function, as an
argument, or as the last argument. There is also a case not given in the semantics:
the list of arguments might be empty.
233a. hfill hole in fr‑>form.apply and step to the next state 233ai≡ (228b)
if (fr‑>form.apply.fn‑>alt == HOLE) { // Small‑Step‑Apply‑First‑Arg
*fr‑>form.apply.fn = mkLiteralStruct(v);
e = head_replaced_with_hole(fr‑>form.apply.actuals);
if (e)
goto exp; // Small‑Step‑Apply‑First‑Arg
§3.6
else The interpreter
goto apply_last_arg; // empty list of arguments
233
} else {
e = transition_explist(fr‑>form.apply.actuals, v);
if (e)
goto exp; // Small‑Step‑Apply‑Next‑Arg
else goto
apply_last_arg; // Small‑Step‑Apply‑Last‑Arg
}
apply_last_arg: // Small‑Step‑Apply‑Last‑Arg (or no arguments)
happly fr‑>form’s fn to its actuals; free memory; step to next state 233bi
Once the overwritten fr‑>form.apply.actuals and fr‑>form.apply.fn are
converted to values, their memory is freed. The frame is popped, and the func‐
tion is applied.
233b. happly fr‑>form’s fn to its actuals; free memory; step to next state 233bi≡ (233a)
{
Value fn = asLiteral (fr‑>form.apply.fn);
Valuelist vs = asLiterals(fr‑>form.apply.actuals); copyEL 231d
free (fr‑>form.apply.fn); evalstack 227a
freeEL(fr‑>form.apply.actuals); type Exp A
type Explist S309b
popframe(evalstack); fr 227a
freeEL 231d
head_replaced_
switch (fn.alt) { with_hole 231c
case PRIMITIVE: mkApplyStruct
happly fn.primitive to vs and step to the next state 233ci A
case CLOSURE: mkHole A
hsave env; bind vs to fn.closure’s formals; step to evaluation of fn’s body 234ai mkLiteralStruct
A
default:
popframe 224b
runerror("%e evaluates to non‑function %v in %e",
pushframe 224b
fr‑>syntax‑>apply.fn, fn, fr‑>syntax); runerror 47a
} topframe 224c
} transition_
explist 231b
A primitive is applied in the standard way. type Value A
233c. happly fn.primitive to vs and step to the next state 233ci≡ (233b) type Valuelist
v = fn.primitive.function(fr‑>syntax, fn.primitive.tag, vs); S309c
freeVL(vs);
goto value;
vf = (|LAMBDA(hx1 , . . . , xn i, ec ), ρc |)
ℓ1 , . . . , ℓn ∈
/ dom σ (and all distinct) .
hvn , ρ, σ, APPLY(vf , v1 , . . . , vn−1 , •) :: Si →
hec , ρc {x1 →
7 ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }, ENV(ρ, CALL) :: Si
(SMALL‐STEP‐APPLY‐LAſT‐ARG)
Before the body of the closure is evaluated, the environment is saved on the stack.
234a. hsave env; bind vs to fn.closure’s formals; step to evaluation of fn’s body 234ai≡ (233b)
{
Namelist xs = fn.closure.lambda.formals;
To evaluate a LET or LETREC that has no bindings, evaluate its body in environ‐
ment ρ.
(EMPTY‐LET)
hLET(hi, e), ρ, σi ⇓ he, ρ, σi
234b. he‑>letx contains no bindings 234bi≡ (228a)
e‑>letx.xs == NULL && e‑>letx.es == NULL
234c. hcontinue by evaluating the body of the let or letrec 234ci≡ (228a)
e = e‑>letx.body;
goto exp;
To evaluate a nonempty LET expression, push a frame in which the first right‐
hand side e1 is replaced by a hole, and start evaluating e1 .
x1 , . . . , xn all distinct
(SMALL‐STEP‐LET)
hLET(hx1 , e1 , . . . , xn , en i, e), ρ, σ, Si →
he1 , ρ, σ, LET(hx1 , •, x2 , e2 , . . . , xn , en i, e) :: Si
The frame is built using a copy of e‑>letx.es, and the hole is inserted into the copy
by calling head_replaced_with_hole.
234d. hstart LET e‑>letx and step to the next state 234di≡ (228a)
pushframe(mkLetxStruct(e‑>letx.let, e‑>letx.xs,
copyEL(e‑>letx.es), e‑>letx.body),
evalstack);
fr = topframe(evalstack);
e = head_replaced_with_hole(fr‑>form.letx.es);
assert(e);
goto exp;
x1 , . . . , xn all distinct
ei has the form LAMBDA(· · ·), 1 ≤ i ≤ n
ℓ1 , . . . , ℓn ∈ / dom σ (and all distinct)
ρ′ = ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }
σ0 = σ{ℓ1 7→ unspecified, . . . , ℓn 7→ unspecified} .
hLETREC(hx1 , e1 , . . . , xn , en i, e), ρ, σ, Si →
he1 , ρ′ , σ0 , LETREC(hx1 , •, x2 , e2 , . . . , xn , en i, e) :: ENV(ρ, NONCALL) :: Si
(SMALL‐STEP‐LETREC)
To evaluate LETREC, save the environment on the stack, then extend it with fresh
locations that are given unspecified values. Then, as for LET, push a LETREC frame,
replace the first right‐hand side with a hole, and start evaluating it. As in Chapter 2,
the right‐hand sides are confirmed to be LAMBDAs at parse time.
235a. hstart LETREC e‑>letx and step to the next state 235ai≡ (228a)
pushenv_opt(env, NONCALL, evalstack);
hbind every name in e‑>letx.xs to an unspecified value in env S357di
pushframe(mkLetxStruct(e‑>letx.let, e‑>letx.xs,
copyEL(e‑>letx.es), e‑>letx.body),
evalstack);
fr = topframe(evalstack); §3.6
e = head_replaced_with_hole(fr‑>form.letx.es); The interpreter
assert(e);
235
goto exp;
,
hv, ρ, σ, LET(hx1 , v1 , . . . , xi , •, xi+1 , ei+1 , . . . , xn , en i, e) :: Si →
hei+1 , ρ, σ, LET(hx1 , v1 , . . . , xi , v, xi+1 , •, . . . , en i, e) :: Si
(SMALL‐STEP‐NEXT‐LET‐EXP)
ℓ1 , . . . , ℓ n ∈
/ dom σ (and all distinct) .
hv, ρ, σ, LET(hx1 , v1 , . . . , xn , •i, e) :: Si →
he, ρ{x1 7→ ℓ1 , . . . , xn 7→ ℓn }, σn {ℓ1 7→ v1 , . . . , ℓn 7→ vn }, ENV(ρ, NONCALL) :: Si
asLiterals 232a
(SMALL‐STEP‐LET‐BODY)
bindalloclist
Both rules are implemented by function transition_explist, which puts v in 153c
the hole and moves the hole. But if the hole is in last position, implementing the checkargc 47c
SMALL‐STEP‐LET‐BODY rule is tricky. Before the frame LET(hx1 , v1 , . . . , xn , •i, e) copyEL 231d
env 227a
is popped, names x1 , . . . , xn are used to update the environment ρ, but after the evalstack 227a
LET frame is popped, the original ρ needs to be saved on the stack. The steps are type Explist S309b
shown by numbered comments in the code. fn 233b
fr 227a
235b. hcontinue with let frame fr‑>form.letx 235bi≡ (228b)
freeEL 231d
e = transition_explist(fr‑>form.letx.es, v); freeVL 232b
if (e) { // Small‑Step‑Next‑Let‑Exp head_replaced_
goto exp; with_hole 231c
} else { // Small‑Step‑Let‑Body lengthNL A
Namelist xs = fr‑>form.letx.xs; // 1. Remember x's and v's lengthVL A
mkLetxStruct
Explist es = fr‑>form.letx.es;
A
Valuelist vs = asLiterals(es);
type Namelist
e = fr‑>form.letx.body; // 2. Update e 43a
popframe(evalstack); // 3. Pop the LET frame popframe 224b
pushenv_opt(env, NONCALL, evalstack); // 4. Push env pushenv_opt 224d
env = bindalloclist(xs, vs, env); // 5. Make new env pushframe 224b
topframe 224c
freeEL(es); // 6. Recover memory
transition_
freeVL(vs);
explist 231b
goto exp; // 7. Step to next state type Valuelist
} S309c
The expressions in a LETREC are already evaluated in an extended environ‐
ment, so when the last expression is evaluated, the only step needed before evalu‐
ating the body is to update the store.
3 } else { // Small‑Step‑Letrec‑Body
hput values in fr‑>form.letx.es in locations bound to fr‑>form.letx.xs 236bi;
freeEL(fr‑>form.letx.es);
Control operators e = fr‑>form.letx.body;
and a smallstep popframe(evalstack);
semantics: µScheme+ goto exp;
}
236
236b. hput values in fr‑>form.letx.es in locations bound to fr‑>form.letx.xs 236bi≡ (236a)
{
Namelist xs = fr‑>form.letx.xs;
Explist es = fr‑>form.letx.es;
while (es || xs) {
assert(es && xs);
assert(find(xs‑>hd, env));
*find(xs‑>hd, env) = asLiteral(es‑>hd);
es = es‑>tl;
xs = xs‑>tl;
}
}
The LET and APPLY forms both save environments on the stack. When the
youngest frame on the stack is an ENV frame, the saved environment is restored
by assigning it to env. In this context, the tag is ignored.
Only the long‑label, long‑goto, and return forms are interpreted in eval. The
other control operators are implemented by lowering.
To evaluate a label, save the current environment and the label on the stack,
then evaluate the body.
When the youngest frame on the stack is a LONG‐LABEL frame, it is simply popped.
(LABEL‐UNUſED)
hv, ρ, σ, LONG‐LABEL(L, •) :: Si → hv, ρ, σ, Si
236e. hpop the stack and step to the next state 236ei≡ (228b)
popframe(evalstack);
goto value;
The label’s purpose is to serve as a target for LONG‐GOTO. To evaluate a LONG‐
GOTO, push a LONG‐GOTO frame, then evaluate the body. (For an alternative se‐
mantics, see Exercise 12.)
(GOTO)
hLONG‐GOTO(L, e), ρ, σ, Si → he, ρ, σ, LONG‐GOTO(L, •) :: Si
237a. hstart e‑>long_goto and step to the next state 237ai≡ (228a)
pushframe(mkLongGotoStruct(e‑>long_goto.label, hole), evalstack);
§3.6
e = e‑>long_goto.exp;
The interpreter
goto exp;
Once the body of the LONG‐GOTO has been evaluated and the youngest frame on 237
the stack is LONG‐GOTO(L, •), the machine starts looking for a target label. It uses
these two rules:
F =6 LONG‐LABEL(L, •) ,
hv, ρ, σ, LONG‐GOTO(L, •) :: F :: Si → hv, ρ, σ, LONG‐GOTO(L, •) :: Si
(GOTO‐UNWıND)
.
hv, ρ, σ, LONG‐GOTO(L, •) :: LONG‐LABEL(L, •) :: Si → hv, ρ, σ, Si
(GOTO‐TRANſFER)
To implement the rules, the interpreter pops the stack, setting fr to the next
youngest frame. If fr points to the label L, it too is popped, and the transfer is com‐
plete. Otherwise, *fr is overwritten with LONG‐GOTO(L, •), effectively unwinding
one frame from the stack, and evaluation continues.
asLiteral 232a
237b. hunwind v to the nearest matching long‑label 237bi≡ (228b) env 227a
{ Name label = fr‑>form.long_goto.label; evalstack 227a
popframe(evalstack); // remove the LONG_GOTO frame type Explist S309b
fr = topframe(evalstack); // fr now points to the next youngest frame find 153b
fr 227a
if (fr == NULL) {
freeEL 231d
runerror("long‑goto %n with no active long‑label for %n",
hole S357c
label, label); mkLongGotoStruct
} else if (fr‑>form.alt == LONG_LABEL && A
fr‑>form.long_label.label == label) { mkLongLabelStruct
popframe(evalstack); A
mkReturnxStruct
goto value;
A
} else {
type Name 43a
fr‑>form = mkLongGotoStruct(label, hole); type Namelist
goto value; 43a
} popframe 224b
} pushenv_opt 224d
pushframe 224b
Like a long‑goto, a return pushes a frame, then evaluates its expression. runerror 47a
topframe 224c
(RETURN) transition_
hRETURN(e), ρ, σ, Si → he, ρ, σ, RETURN(•) :: Si explist 231b
237c. hstart e‑>returnx and step to the next state 237ci≡ (228a)
pushframe(mkReturnxStruct(hole), evalstack);
e = e‑>returnx;
goto exp;
Once a return’s expression has been evaluated and the youngest frame on the stack
is RETURN(•), the machine unwinds the stack until it finds an ENV frame with a
CALL tag. The implementation is left for you, as Exercise 20.
237d. hreturn v from the current function [[prototype]] 237di≡
runerror("Implementation of (return e) is left as an exercise");
3.6.11 Implementing proper tail calls
Environment frames are tagged so that tail calls can be optimized (Section 2.14.2,
page 170). The optimization ensures that a function called in a tail context reuses
3 the stack space of its caller. Tail contexts are determined by the syntactic structure
of a function’s body, but an expression evaluated in a tail context is evaluated in a
dynamic context—that is, a run‐time machine state—in which the top of the stack
Control operators
holds one or more ENV frames, at least one of which has a CALL tag (Exercise 16).
and a smallstep
Using the semantics in this chapter, tail‐call optimization is easy to express;
semantics: µScheme+
tail calls are optimized if the evaluation stack never holds consecutive ENV frames.
238 For example, any evaluation stack of the form ENV(ρ2 , CALL) :: ENV(ρ1 , CALL) :: S
should be optimized to ENV(ρ1 , CALL) :: S . (The environments ρ1 and ρ2 are num‐
bered in the order in which they are pushed on the stack.) So if the youngest frame
on the stack is an ENV(ρ1 , CALL), the interpreter does not push ENV(ρ2 , CALL).
This optimization is justified by two metatheoretic claims:
• If e tries to unwind the stack, a long‑goto unwinds two call frames as easily
as one. A return behaves a little more differently in the two cases, but they
wind up in the same state.
In the examples above, every ENV frame has a CALL tag, but to get true, proper
tail calls in all circumstances, stacks involving any mix of tags must also be opti‐
mized. Working out the semantics is Exercise 14; the code looks like this:
238. hevalstack.c 227ai+≡ ◁ 227a
void pushenv_opt(Env env, SavedEnvTag tag, Stack s) {
assert(s);
Frame *f = optimize_tail_calls ? topframe(s) : NULL;
if (f && f‑>form.alt == ENV) { // don't push a new frame
if (tag == CALL && f‑>form.env.tag == NONCALL)
f‑>form.env.tag = CALL;
} else {
pushframe(mkEnvStruct(env, tag), s);
}
}
3.7 STACĸſ, CONTROL, AND ſEMANTıCſ Aſ THEY REALLY ARE
The evaluation stack used in µScheme+’s semantics and interpreter is closely re‐
lated to stacks used in real languages. The key difference lies in the representation
of intraprocedural control. In real languages, control information is stored in a §3.7
sequence of instructions for a real or virtual machine, and control transfer is im‐ Stacks, control,
plemented by conditional or unconditional branch instructions. In µScheme+, and semantics as
by contrast, control information is stored on the evaluation stack, and control they really are
transfer is implemented by popping a frame off the stack. For example, a real 239
if expression is implemented by testing a condition register and using its state to
transfer control to a labeled instruction sequence for expression e2 or e3 , using a
conditional branch instruction. In µScheme+, v acts like a condition register, but
the destination of the control transfer (e2 or e3 ) is taken off the evaluation stack.
Although real languages handle intraprocedural control in different ways from
µScheme+, real implementations do use stacks that look like special cases of our
evaluation stack. For example, a language that supports recursive procedures uses
a call stack. A call stack saves the locations and/or values of local variables, just like
the ENV frames in our evaluation stack. A call stack also saves control information
between procedures—at compile time, a procedure doesn’t know who its caller is, so
that information can’t be compiled in. Control information on a call stack usually
takes the form of a return address, which points into the instruction sequence of a
calling procedure.
Real tail‐call optimization works just like our model: when f calls g and g makes
an optimized tail call to h, g doesn’t push a new frame on the call stack. Instead,
g arranges for h to see f ’s frame, and when h returns, it returns directly to f .
Some real implementations use a stack to evaluate function applications and
similar expressions. Such an implementation includes an operation like “evalu‐
ate e and push its value on the stack.” Using this operation, an application like
(+ e1 e2 ) would be implemented by first pushing the value of e1 , then pushing the
value of e2 , then adding the top two values on the stack. This technique is used
in the Java Virtual Machine as well as in common microcontroller hardware. And
in some languages, such stack‐based computation is exposed directly to program‐
mers; my two favorites are PostScript and Forth.
type Env 153a
type Frame 223
3.7.2 Control operators mkEnvStruct A
optimize_tail_
The control operators break, continue, and return, as depicted in this chapter, are calls 224f
provided by many languages. Sometimes break is extended so it can exit a nest of pushframe 224b
several loops. For example, in the POSIX shell language, break may take a numeric type SavedEnvTag
226b
argument that says how many nested loops to exit; in Ada, a loop can be named with type Stack 223
a label, and a break statement can use that label. topframe 224c
Although break, continue, and return are often used in the same way as in
µScheme+, they are not implemented in the same way. Any compiler, and most
interpreters, will use a static analysis—which means “looking at the code”—based on
a proof system like the one you can create in Exercise 18. The static analysis keeps
track of the possible evaluation contexts at every position in the source code, and
it uses this information to lower break, continue, and return into simple, “short”
goto instructions, instead of our “long” ones. Such instructions don’t inspect the
stack at run time.
Forms like try‑catch and throw are found in dozens of real languages, usually
under the name exceptions. There are many refinements.
• A try‑catch can be associated with “cleanup” code that executes once the
Small‐step semantics are widely used. One reason is they enable a powerful proof
technique: if a property holds of every initial state, and if that property is preserved
by every state transition permitted under the semantics, then that property holds
of every reachable state. This proof technique is quite good at proving safety prop‐
erties, which say that good programs don’t do bad things. One example is memory
safety; in a memory‐safe program, the condition ℓ ∈ dom σ is always satisfied. Ex‐
amples of languages that are memory‐safe include full Scheme and Standard ML.
Examples of languages that are not memory‐safe include C and C++. Unsafe access
to memory can lead to bugs, blue‐screen crashes, and security exploits.
Small‐step semantics are also widely used because they can easily be extended
to talk about actions taken by programs, such as I/O and communication. For ex‐
ample, the state‐transition arrow can be labeled. Labels can say things like this:
• The transition causes the abstract machine to write a character to standard §3.7
output. Stacks, control,
and semantics as
• The transition causes the abstract machine to send a message. they really are
• The transition causes the abstract machine to consume a character from 241
standard input; the value of the character is bound to the name c.
• The transition happens only when the abstract machine receives a message,
and the contents of the message are bound to the name m.
Small‐step semantics come in many forms—not all state machines use seman‐
tic objects as part of their states. If the components of a state machine are purely
syntactic, its semantics is a reduction semantics. In the most extreme form, the en‐
tire state is an expression or other syntactic term in some programming language;
the language described by such a semantics is usually called a calculus. In a cal‐
culus, each transition rewrites one syntactic form into another syntactic form;
such a transition is usually called a reduction. Calculi worth knowing about in‐
clude Church’s λ‐calculus, which models functional programming, and Milner’s
π ‐calculus, which models mobility and communication (Rojas 2015; Milner 1999).
Calculi often have nondeterministic semantics: a machine might have a choice
about which state it can transition to. For example, Church’s λ‐calculus has an
important choice when a lambda abstraction is applied: the abstract machine can
try to reduce the function’s argument or it can substitute the argument into the
function’s body. Church and Rosser proved that either choice can lead to the same
final result: no matter what reduction the machine chooses, it can choose future
reductions such that from any reachable state, there are sequences of reductions
that eventually arrive at the same state. But the choice makes a big difference in
the way implementations perform and behave: a machine that reduces the func‐
tion’s argument first corresponds to a strict or eager evaluation strategy, as in ML or
Scheme, and a machine that substitutes into the function’s body first corresponds
to a lazy evaluation strategy, as in Haskell.
The semantics of Core µScheme+ is intended to be deterministic. That is, in any
given state, the next state is completely determined—unless the abstract machine
is stuck or in a final state, it transitions to exactly one next state. A deterministic
semantics determines an evaluation strategy, which, in a calculus, can be specified
using an evaluation context. This is a data structure or a chunk of syntax that locates
a currently evaluated expression in a larger syntactic context. For example, eval‐
uation contexts for Core µScheme+ can be described by this (partial) grammar;
a context is C , a value is v , an expression is e, and a name is x or L:
C ::= •
| (set x C )
| (if C e2 e3 )
| (C e 1 · · · e n )
| (v v1 · · · vi−1 C ei+1 · · · en )
| (let ([x1 v1 ] · · · [xi−1 vi−1 ] [xi C ] [xi+1 ei+1 ] · · · [xn en ]) e)
| (return C )
| (long‑label L C )
| (long‑goto L C )
The evaluation contexts specified by this grammar enforce not only an eager eval‐
uation strategy but also an evaluation order: in the evaluation context for function
application, everything to the left of the hole is guaranteed to be a value; only things
to the right of the hole can be expressions. The context for let is similar. The eval‐
3 uation contexts ensure that actual parameters and right‐hand sides are evaluated
from left to right.
Evaluation contexts are closely related to evaluation stacks. And the grammar
Control operators
of contexts can be converted to a grammar of frames; simply replace each nested
and a smallstep
context with a hole:
semantics: µScheme+
F ::= (set x •)
242 | (if • e2 e3 )
| (• e1 · · · en )
| (v v1 · · · vi−1 • ei+1 · · · en )
| (let ([x1 v1 ] · · · [xi−1 vi−1 ] [xi •] [xi+1 ei+1 ] · · · [xn en ]) e)
| (return •)
| (long‑label L •)
| (long‑goto L •)
Each stack of frames corresponds to a unique evaluation context, and vice versa.
As described in Exercise 15, the correspondences can be implemented as a pair of
functions.
3.7.4 Continuations
vf = CONTıNUATıON(S ′ ) .
hv1 , ρ, σ, APPLY(vf , •) :: Si → hv1 , ρ, σ, S ′ i
(APPLY‐UNDELıMıTED‐CONTıNUATıON)
The “undelimited” part of the name is explained at the end of this section.
• The current continuation is captured by a new primitive, call/cc. “Capture”
means “make a copy of.”
hv1 , ρ, σ, APPLY(PRıMıTıVE(call/cc), •) :: Si →
hAPPLY(v1 , CONTıNUATıON(S)), ρ, σ, Si
(CALL‐WıTH‐CURRENT‐CONTıNUATıON)
As an example, suppose f is the function (lambda (k) (k 5)). Then the ex‐ §3.8
pression (+ (call/cc f) 4) evaluates to 9. That’s because the continuation cap‐ Summary
tured by call/cc is roughly APPLY(PRıMıTıVE(+), •, LıTERAL(4)), and it behaves
like the function (lambda (x) (+ x 4)). So (call/cc f) behaves like the expression 243
(f (lambda (x) (+ x 4))), which evaluates to 9.
In theory, call/cc is a magnificent operation because it can be used to im‐
plement a remarkable range of control operators: coroutines, backtracking, Icon‐
style generators, nondeterministic choice, setjmp/longjmp, and more. (Think of
call/cc as setjmp enhanced with mutant superpowers.) As just one example,
my personal favorite, call/cc can be used to implement concurrent processes
with message passing (Haynes, Friedman, and Wand 1984; Ramsey 1990). Using
call/cc appeals to theorists and other minimalists because call/cc is all you need;
a ton of other language features and control operators can simply be lowered into
call/cc. And call/cc can be fun to program with. But in practice, call/cc is not
so great. It’s not widely implemented—copying an entire stack is expensive, and
that expense can be avoided only if your implementation is really clever. More‐
over, many features that might be lowered into call/cc don’t actually use its full
power, which includes the power to enter a single continuation multiple times.
And the implementation problems pale beside the modularity problems: A con‐
tinuation might look like a function, but it doesn’t behave like a function. In particu‐
lar, it doesn’t compose with other functions: The composition f ◦g makes it look like
f will be applied to whatever result g returns, but if g is a captured continuation,
the application of f is part of the context S that is destroyed when g is applied.
Continuations that are captured with call/cc are called undelimited. A call/cc
can grab an unbounded amount of context, and when the resulting continuation is
entered, it can discard an unbounded amount of context. An interesting alterna‐
tive, which addresses some of the modularity problems, is a delimited continuation;
for example, a delimited continuation might capture just a portion of the evaluation
stack, up to a delimiter that works a bit like long‑label, and turn it into a function.
Delimited continuations are mentioned below as possible further reading.
3.8 SUMMARY
Control operators can be awkward to handle with a big‐step semantics. And big‐
step semantics cannot describe a computation that continues forever, like an op‐
erating system or a web server. These situations call for a small‐step semantics,
in which each step “reduces” a term or the state of a machine, and a sequence of
reductions may end or may continue forever.
A small‐step semantics for a functional language with imperative features, like
µScheme or µScheme+, can use an abstract machine with four components: a con‐
trol component e or v , an environment ρ, a store σ , and a continuation S . Such
a machine is called a CESK machine. In our machine, the continuation S is rep‐
resented as a stack of frames; a frame is (usually) an expression with a hole in it.
Control operators are implemented by inspecting the evaluation stack and discard‐
ing or copying some of the frames found there. And at any point during evaluation,
the stack can be used to convert the machine’s state into an incompletely evaluated
expression: use e or v to fill the youngest hole in S , then use the result to fill the
next youngest hole, and so on.
Although the details are beyond the scope of this book, small‐step semantics
can simplify proofs of metatheoretical results, like “well‐typed programs don’t go
3 wrong.” Small‐step proofs can be simpler than big‐step proofs, because a small‐
step proof usually reasons about the effects of a single step, whereas a big‐step
proof uses an induction hypothesis to reason about entire derivations.
Control operators
and a smallstep
semantics: µScheme+ 3.8.1 Key words and phrases
CONTROL OPERATOR A syntactic form that transfers control outside its immedi‐
ate context—that is, away from its parent in the abstract‐syntax tree. Con‐
trol operators that transfer control within a procedure, including break,
continue, and return, are easily dealt with by a compiler. Control opera‐
tors that transfer control between procedures—like throw, longjmp, or rais‐
ing an exception—require run‐time support, sometimes involving inspection
of the CALL ſTACĸ. Control operators that capture a continuation, including
call/cc and CONTROL, typically require the implementation to turn the call
stack into a data structure. All control operators can be specified using a
ſMALL‐ſTEP ſEMANTıCſ. Control operators in the first two categories can
be specified by extending a BıG‐ſTEP ſEMANTıCſ with behaviors, as done in
Chapter 10.
REDEX Short for “reducible expression,” a redex is a form that appears on the left‐
hand side of a reduction rule in a REDUCTıON ſEMANTıCſ.
The most influential complaint ever made about control operators was a note writ‐
ten by Edsger Dijkstra and published in Communications of the ACM as a “letter to
the editor” entitled “Go To Statement Considered Harmful” (Dijkstra 1968). Dijkstra
advocated for structured programming, and his later monograph on the topic was
collected in book form along with monographs by Ole‐Johan Dahl and Tony Hoare
(Dahl, Dijkstra, and Hoare 1972). Dijkstra eventually supplemented his ideas about
3 program structure with deep, powerful ideas about semantics and proofs of pro‐
grams using weakest preconditions, a form of predicate transformer that relies on the
absence of control operators, even such well‐behaved ones as break and continue
Control operators
(Dijkstra 1976). Weakest preconditions, which Dijkstra uses to define his language,
and a smallstep
still offer a powerful technique for creating correct procedural programs. Many
semantics: µScheme+
examples can be found in Dijkstra’s (1976) book, which shows polished gems cre‐
246 ated by a master programmer. While the gems are worth marveling at, if you want
to learn to apply the techniques yourself, you might start with Bentley (1983) and
Gries (1981).
In modern programming, the essential control operators are the ones involv‐
ing exceptions. Exceptions can be implemented with acceptably low overhead;
the techniques are well known and are described by Drew, Gough, and Leder‐
mann (1995). Exceptions can also be implemented, with greater overhead, using
setjmp and longjmp; two good examples are presented by Roberts (1989) and Han‐
son (1996, chapter 4).
Continuations and ideas related to continuations can be found in much of the
research of the late 1960s and early 1970s (Reynolds 1993). But continuations be‐
came widely known in a form developed by Christopher Wadsworth under the su‐
pervision of Christopher Strachey (Strachey and Wadsworth 2000). Using continu‐
ations, Wadsworth and Strachey found a clean way to give semantics to goto state‐
ments. This work fit into a larger program, led by Strachey and by Dana Scott, on
what is now called denotational semantics. The results combined Scott’s mathemati‐
cal insights, which identified a well‐behaved lattice of mathematical functions that
could be used to explain recursion, with Strachey’s method of defining the deno‐
tation of an expression (that is, what mathematical object the expression stands
for) by composing the denotations of its subexpressions. Continuations have since
been the method of choice for explaining many computational ideas connected to
control. The work is definitively described by Stoy (1977), an excellent book for the
mathematically inclined. The less mathematically inclined may prefer textbooks
written by Allison (1986) or Schmidt (1986).
“Proper tail calls” are usually defined informally, but a precise, careful defini‐
tion is presented by Clinger (1998)—using a semantics very similar to the one in this
chapter.
Abstract machines are not just a semantic technique; they are commonly used
in programming‐language implementation. An implementor designs a machine
that can be implemented relatively efficiently, then builds a compiler that translates
source programs into code for that machine. Machines that have been used in this
way are surveyed by Diehl, Hartel, and Sestoft (2000).
Small‐step semantics are explained in depth by Felleisen, Findler, and Flatt
(2009), who present major varieties of reduction semantics, including the CESK ma
chine on which the semantics of µScheme+ is based. They also present standard
theoretical results, of which the most important—the Church‐Rosser theorem—
says essentially that in standard reduction semantics, nondeterminism in the re‐
duction relation is harmless. Finally, they present Redex, a tool for experimenting
with reduction semantics.
Reduction semantics, abstract machines, and interpreters for big‐step seman‐
tics are more closely related than you might think—so closely that an interpreter
in the style of Chapter 2 (or more likely, Chapter 5) can be transformed automat‐
ically into an abstract‐machine semantics. These relationships have been eluci‐
Table 3.5: Synopsis of all the exercises, with most relevant sections
dated most deeply by Olivier Danvy, his students, and his collaborators; one point
of entry is Danvy’s (2006) DSc thesis.
Delimited continuations and a rationale for their design are described by
Felleisen (1988), who presents the control operators # and F (now called prompt
and control). The closely related operators shift and reset are described by
Danvy and Filinski (1990), who include several programming examples. The shift
and reset operators have engendered significant interest, including a nice tutorial
with many exercises (Asai and Kiselyov 2011).
Many examples of programming with undelimited continuations are given by
Friedman, Haynes, and Kohlbecker (1984), who use call/cc as the main control
primitive. The expressive power of call/cc is demonstrated by Filinski (1994),
who shows that a wide variety of abstractions, including shift and reset, can be
implemented using call/cc and a single mutable storage cell. Unfortunately, Fil‐
inski’s insight is more mathematical than practical; great care is required with as‐
sumptions and corner cases (Ariola, Herbelin, and Herman 2011). A case against
call/cc is presented by Kiselyov (2012), who argues that Filinski’s techniques are
brittle because their assumptions are rarely satisfied by large, practical systems.
Unless you build your entire compiler around continuations, like Appel (1992),
call/cc is challenging to implement. The challenges are described and met
by Bruggeman, Waddell, and Dybvig (1996) and by Hieb, Dybvig, and Brugge‐
man (1990). Even though they omit lots of details, these papers are really good.
A broader view of implementation techniques is provided by Clinger, Hartheimer,
and Ost (1999).
Delimited continuations are also challenging; an implementation of shift and
reset in the Scheme48 virtual machine is nicely described by Gasbichler and Sper‐
ber (2002). The shift and reset operators have also been implemented in a native‐
code compiler for MinCaml (Masuko and Asai 2009).
If you want a feel for calculi, I recommend Milner’s (1999) monograph on the
π ‐calculus; it combines readily accessible examples of concurrent and commu‐
nicating systems with the mathematical foundations needed to prove interesting
properties. Parts of the book are heavily mathematical, but on a first reading, you
can skip the theory chapters.
3.9 EXERCıſEſ
The exercises are arranged mostly by the skill they call on (Table 3.5). The best
exercises involve implementing control operators and reasoning about tail calls.
3 • In Exercises 13, 14, and 16, you use the operational semantics to justify tail‐
call optimization and to explain how to optimize evaluation stacks.
Control operators
and a smallstep • In Exercises 20 and 21, you implement return and you extend µScheme+
semantics: µScheme+ with unwind‑protect.
A. In C you can write an if statement with no else. When you want to write
the same kind of control structure in µScheme+, what form of expression do
you use?
B. If a throw is evaluated inside a try‑catch and the labels match, what happens?
C. If a throw is evaluated inside a function with no try‑catch, what happens?
D. If a throw is evaluated inside a try‑catch but the labels don’t match, what hap‐
pens?
E. What judgment or judgments of small‐step semantics correspond to a single
judgment of big‐step semantics?
F. How many rules of small‐step semantics are needed to specify what a set ex‐
pression does? What does each rule do?
G. When the current item of the abstract machine is a value, most rules inspect
only the topmost (youngest) frame on the evaluation stack. What forms of
youngest frame trigger the inspection of older frames?
H. What is the name of the optimization implemented by pushenv_opt? What
does the optimization accomplish?
I. When a frame on the evaluation stack holds an expression with a hole in it,
where is the memory allocated for the struct Exp?
J. When you want to tell the interpreter to show you how the stack changes as an
expression is evaluated, what do you do?
1. Iteration over lists with early exit. Many recursive functions that consume lists
can also be implemented using loops and control operators. Use loops and
control operators, but no recursion, to implement functions exists?, all?,
and member? from Chapter 2.
2. Early exit without control operators. Procedural code can be written in the style
of “structured programming,” that is, using loops but no control operators.
(a) Is there a simpler expression that behaves like (return (return 1)) in
every context?
(b) Is there a simpler expression that behaves like (return (continue)) in
every context?
§3.9
(c) Is there a simpler expression that behaves like (return (break)) in ev‐ Exercises
ery context?
249
(d) Is there a simpler expression that behaves like (throw :L (return 3))
in every context?
(e) Is there a simpler expression that behaves like (return (throw :L 4))
in every context?
(f) Can expression (begin e1 e2 · · · (throw L exn ) · · · en ) be replaced
by (begin e1 e2 · · · (throw L exn )) without changing the behavior of
the program?
(g) Can the function application (e e1 e2 · · · (throw L exn ) · · · en ) be
simplified without changing the behavior of the program? If so, what
is the simpler version? If not, why not?
(a) (try‑catch
(try‑catch (throw :h 'inner)
:h
(lambda (exn) (list2 'caught exn)))
:h
(lambda (exn) (list2 'outer‑caught exn)))
(b) (try‑catch
(throw :h (try‑catch 'terminated :h
(lambda (exn) (list2 'inner exn))))
:h
(lambda (exn) (list2 'outer exn)))
5. Eliminating break from µScheme code. A control operator like break can be
eliminated by a program transformation. (Eliminating unloved control con‐
structs by translating them to while loops and conditions was a nerdy pas‐
time of the 1960s.) Generalize your knowledge from Exercise 2 by defining
a program transformation that takes as input a µScheme program that uses
break and produces as output an equivalent µScheme program that does not
use break.
Define your program transformation as a µScheme function that takes two
inputs: a µScheme expression to be transformed, represented as an S‐
expression, and a list of names that are guaranteed not to appear in the
program to be transformed (and can therefore safely be used as variables).
To represent a µScheme expression as an S‐expression, simply put a quote
mark in front of its source code.
6. Context for lowering break and continue. If a break or continue appears out‐
side any loop, my interpreter reports a “lowering error.”
(a) Write a proof system for the judgment “e is inside a loop.” The body of
7. Lowering with condition outside the loop. The lowering transformation defined
by Table 3.4 treats break in such a way that when (while e1 e2 ) is evaluated,
executing a break within e1 terminates the loop. In other words, the lower‐
ing rules treat e1 as if it were inside the loop.
These rules offend me. I think a break within e1 should terminate the en
closing loop, if any. Define a new lowering transformation that fixes the
problem. I recommend passing this transformation a long list of labels that
are all distinct from one another and distinct from the labels :continue and
:return.
Test your transformation either by implementing it in µScheme or by modi‐
fying the C code in Section M.3.
8. Stack requirements for foldl and foldr. A list of 10 numbers can be summed
using either foldl or foldr.
9. Implement map using constant stack space. Implement a new version of map with
the following property:
You will have to make sure that every recursive call occurs in tail position, and
you may have to use accumulating parameters.
10. Compare stack consumption in µScheme and µScheme+. Write a µScheme func‐
tion and call that make the µScheme interpreter halt with a “recursion too
deep” error, but which the µScheme+ interpreter runs just fine.
3.9.5 The evaluation stack in the semantics
11. Distinguishing savedenvironment frames. A frame formed with ENV always re‐
stores the environment, independent of its tag (CALL or NONCALL). So what
does the tag do, and in which states is it possible to distinguish different tags?
Considering machine states with one of these frames on top of the evaluation
stack, answer these two questions:
(a) Can you choose an environment ρ, a store σ , a stack S , a value v , and §3.9
a saved environment ρ′ such that hv, ρ, σ, ENV(ρ′ , NONCALL) :: Si re‐ Exercises
duces to a different final state than hv, ρ, σ, ENV(ρ′ , CALL) :: Si?
251
(b) Can you choose an environment ρ, a store σ , a stack S , an expression e,
and a saved environment ρ′ such that he, ρ, σ, ENV(ρ′ , NONCALL) :: Si
reduces to a different final state than he, ρ, σ, ENV(ρ′ , CALL) :: Si?
F is not LONG‐LABEL(L, •) ,
hLONG‐GOTO(L, e), ρ, σ, F :: Si → hLONG‐GOTO(L, e), ρ, σ, Si
(ALTERNATıVE‐GOTO‐UNWıND)
F = LONG‐LABEL(L, •) .
hLONG‐GOTO(L, e), ρ, σ, F :: Si → he, ρ, σ, Si
(ALTERNATıVE‐GOTO‐TRANſFER)
Write an expression of Core µScheme+ that evaluates to #t under the al‐
ternative semantics and to #f under the original semantics. Explain how it
works.
13. Prove that optimizing tail calls is safe. Tail‐call optimization is sound only if
for arbitrary e, v , ρ, σ , ρ1 , ρ2 , and S , any unoptimized stack of the form
ENV(ρ2 , CALL) :: ENV(ρ1 , CALL) :: S can be replaced with the optimized form
ENV(ρ1 , CALL) :: S .
(a) Prove that when the optimized and unoptimized forms are on the top
of the stack and the current item is a value, the two stacks are indistin‐
guishable. To do so, prove that
and
hv, ρ, σ, ENV(ρ1 , CALL) :: Si →∗ hv, ρ1 , σ, Si
(b) If e can be evaluated without control operators, so there exist a σ ′ and v
such that he, ρ, σi ⇓ hv, σ ′ i, we can assume (Exercise 26) that there is
a ρ′ such that for any stack S ′ , he, ρ, σ, S ′ i →∗ hv, ρ′ , σ ′ , S ′ i. Using this
assumption, prove that when the current item is such an expression,
the optimized and unoptimized forms are indistinguishable. That is,
prove that
and
he, ρ, σ, ENV(ρ1 , CALL) :: Si →∗ hv, ρ1 , σ ′ , Si.
(c) To prepare for cases where the optimized and unoptimized stacks
have other frames pushed on top of them, prove that if the evaluation
of he, ρ, σ, Si does not get stuck or loop forever, then exactly one of the
following holds:
3 • he, ρ, σ, Si →∗ hv, ρ′ , σ ′ , Si
• he, ρ, σ, Si →∗ hv, ρ′ , σ ′ , RETURN(•) :: F1 :: F2 :: · · · :: Fn :: Si for
Control operators some (possibly empty) sequence of frames F1 :: F2 :: · · · :: Fn .
and a smallstep • he, ρ, σ, Si →∗ hv, ρ′ , σ ′ , LONG‐GOTO(L, •)::F1 ::F2 ::· · ·::Fn ::Si
semantics: µScheme+ for some (possibly empty) sequence of frames F1 :: F2 :: · · · :: Fn .
252 (d) Prove that if none of F1 :: F2 :: · · · :: Fn has the form ENV(ρ′ , CALL),
then
14. More cases for optimizing tail calls. To implement truly proper tail calls, the
interpreter must simplify every stack that contains consecutive ENV frames,
regardless of tag.
(a) In state he/v, ρ, σ, ENV(ρ2 , NONCALL) :: ENV(ρ1 , CALL) :: Si, how can
the stack be simplified?
(b) In state he/v, ρ, σ, ENV(ρ2 , CALL) :: ENV(ρ1 , NONCALL) :: Si, how can
the stack be simplified?
(c) In state he/v, ρ, σ, ENV(ρ2 , NONCALL) :: ENV(ρ1 , NONCALL) :: Si, how
can the stack be simplified?
(d) ENV frames on the evaluation stack are inspected by rules SMALL‐STEP‐
REſTORE‐ENVıRONMENT, GOTO‐UNWıND, GOTO‐TRANſFER, RETURN‐
UNWıND, and RETURN‐TRANſFER. For each previous part of this prob‐
lem, justify your answer and list the rules you appeal to.
15. The evaluation stack as evaluation context. Grammars for evaluation contexts
and stack frames are given in Section 3.7.3. If a hole is represented by the
µScheme symbol <*>, then both evaluation contexts and stack frames can
be represented as S‐expressions.
Figure 3.6: Partial proof system for tail position (Core µScheme+)
16. Dynamic context of an expression in tail position. A simple proof system that
says when an expression “is in tail position” is shown in Figure 3.6. Using
metatheoretic reasoning about this proof system, prove that if an expression
is in tail position, then when that expression is evaluated, the top of the stack
contains a sequence of zero or more frames of the form ENV(ρ, NONCALL)
followed by a frame of the form ENV(ρ, CALL). Try proof by induction over
a derivation that uses the rules in Figure 3.6.
17. Expressions returned and tail position. It’s tempting to say that the e in
(return e) occurs in tail position. But when e is evaluated, the top of the
stack does not necessarily contain a sequence of zero or more frames of
the form ENV(ρ, NONCALL) followed by a frame of the form ENV(ρ, CALL).
It could be made so, however, by these rules:
(a) Prove that the alternative RETURN rules produce the same results as the
original rules.
(b) Define a function that uses return in such a way that the original rules
produce one result but the alternative rules produce a different result.
18. Occurrence inside a long‑label. Look at Figure 3.6, which defines a little
proof system to say exactly when an expression occurs “in tail position.” Cre‐
ate a similar proof system to say exactly when an expression occurs “inside
a long label L.” Start with these rules:
(a) Using metatheoretic reasoning about this proof system, prove that if
The proof in part (b) guarantees that when break and continue are lowered
to long‑goto expressions, those expressions are evaluated without error.
21. Finalization. When you’re writing procedural code, sometimes you want to
define cleanup code that executes no matter what. The classic example is
that when you open a file, you want to be sure to close it. In many languages,
cleanup code is attached to a try‑catch using the keyword finally. But we’ll
put cleanup code inside a form called unwind‑protect, which comes from
Common Lisp:
The semantics can be expressed in a handful of rules. The rules code “evalu‐
ate finalizer ef and then proceed as you would have” by synthesizing a little
BEGıN expression.
(UNWıND‐PROTECT)
hUNWıND‐PROTECT(eb , ef ), ρ, σ, Si →
heb , ρ, σ, UNWıND‐PROTECT(•, ef ) :: Si
24. Visualizing just the call stack. To diagnose a fault, a visualization of the stack
(“stack trace”) can be very helpful. But an evaluation stack includes so much
information that a complete visualization can be hard to digest. Implement
a visualization that shows just the active function calls.
• Enhance the implementation of the SMALL‐STEP‐APPLY‐LAſT‐ARG rule
so that it copies the syntax field of the APPLY frame into the syntax field
of the ENV frame.
• Write a C function that takes a Stack and shows the syntax fields of the
ENV frames that have CALL tags. Arrange for µScheme’s error primi‐
tive to call that function.
25. Diagnosing a long‑goto with no target. If long‑goto is evaluated without a
corresponding long‑label, the interpreter doesn’t discover the error until
the context is lost. We can do better. Change the interpreter so that if it
evaluates long‑goto, it first checks to make sure that the stack contains a
LONG‐LABEL frame with a matching label. If not, long‑goto should show
what is on the stack, preferably using the function from Exercise 24.
3.9.9 Continuations
26. Continuation capture. Using the rules in Section 3.7.4, implement call/cc.
27. Using captured continuations. Implement try‑catch and throw using call/cc
and possibly an additional data structure.
CHAPTER 4 CONTENTſ
4.1 WHAT GARBAGE ıſ AND 4.5.2 A brief example 274
WHERE ıT COMEſ FROM 258 4.5.3 Prototype of a copying
system for µScheme 276
4.2 GARBAGE‐COLLECTıON
4.5.4 Performance 278
BAſıCſ 259
4.2.1 Performance 260 4.6 DEBUGGıNG A COLLECTOR 280
4.2.2 Reachability and roots 260 4.6.1 An interface for debugging 280
4.2.3 Heap growth 262 4.6.2 Debugging techniques 282
4.3 THE MANAGED HEAP ıN 4.7 MARĸ‐COMPACT
µSCHEME+ 263 COLLECTıON 283
4.3.1 Where are Value objects 4.8 REFERENCE COUNTıNG 283
stored? 263
4.3.2 Reachability of locations 263 4.9 GARBAGE COLLECTıON
4.3.3 Interface to the managed Aſ ıT REALLY ıſ 285
heap: Roots, allocator, 4.10 SUMMARY 287
initialization 264
4.10.1 Key words and phrases 288
4.3.4 Using the heap interface:
4.10.2 Further reading 291
µScheme allocation 266
4.11 EXERCıſEſ 292
4.4 MARĸ‐AND‐ſWEEP
4.11.1 Retrieval practice and
COLLECTıON 266
other short questions 292
4.4.1 Prototype mark‐and‐sweep
4.11.2 Build, measure, and
allocator for µScheme 267
extend the copying
4.4.2 Marking heap objects in
collector 293
µScheme 268
4.11.3 Build, measure,
4.4.3 Performance 270
and extend the
4.5 COPYıNG COLLECTıON 271 mark‐and‐sweep collector 294
4.5.1 How copying collection 4.11.4 Analyze costs of collection 297
works 272 4.11.5 Deeper experimental study 298
Automatic memory management 4
No Scheme object is ever destroyed. The reason that
implementations of Scheme do not (usually!) run out of
storage is that they are permitted to reclaim the storage
occupied by an object if they can prove that the object
cannot possibly matter to any future computation.
Kelsey, Clinger, and Rees (1998, page 3)
A running µScheme program continually allocates fresh locations. How are they
supplied? Memory is limited, and malloc will eventually run out. Memory can be
recovered using free, but if a programmer must call free, as in C and C++, they risk
memory errors: leaks, locations that are freed multiple times, and misuse of freed
locations (so‐called dangling‐pointer errors). Memory errors can make a program
crash—or, worse, silently produce wrong answers. But in languages like µScheme,
full Scheme, Java, and JavaScript, which are memorysafe, such errors are impos‐
sible. The errors are prevented because the implementation of µScheme, not the
µScheme programmer, figures out when it is safe to reuse a location. The tech‐
niques used to reuse locations safely are demonstrated in this chapter.
Memory‐management techniques might seem like matters for implementors,
but automatic memory management enables all kinds of designers to keep things
simple. Language designers can include features that create closures, cons cells,
and other objects without having to say when the memory they occupy must be
reclaimed. Software designers can define interfaces that focus on the needs of the
client; when memory management is automatic, it is not necessary to clutter an in‐
terface with extra functions and/or parameters that address such issues as where
memory is allocated, who owns it, when it is freed, and how a client can make a
private copy. Without automatic memory management, interfaces would be more
complex, programming with higher‐order functions would be nearly impossible,
and object‐oriented systems like Smalltalk (Chapter 10) would be much more com‐
plex. Safety guarantees, like those that Java provides, would be impossible. Auto‐
matic memory management underlies all civilized programming languages.
Automatic memory management is explained in this chapter. The chapter em‐
phasizes garbage collection, the most widely used method of memory management,
and its implementation in µScheme. (The name “µScheme” is used generically;
the language that is actually implemented is µScheme+.) Reference counting, an
alternative method, is discussed only briefly (Section 4.8).
257
4.1 WHAT GARBAGE ıſ AND WHERE ıT COMEſ FROM
The µScheme interpreter uses Value objects in two ways. Temporary values, like
values v and w computed by the implementation of cons on page 161c, are stored
4 in local variables, i.e., locations on the C stack. In cons, for example, local vari‐
ables v and w hold values until they are passed to allocate, and afterward, those
variables are not used again. But some Value objects are stored in locations allo‐
cated on the C heap, with malloc, because we can’t predict how long they will live.
Automatic memory
In particular, every car and cdr of every cons cell is allocated on the C heap, us‐
management
ing function allocate. Why? Because if a µScheme function returns a cons cell,
258 there’s no general way to decide whether that cons cell’s car or cdr will be needed in
a future computation. For the same reason, every location in every environment in
every closure is allocated on the C heap. In C, of course, a programmer would have
to call free at an appropriate moment. In Scheme, memory is freed and reused by
the system.
How can the system know if an object can be reused? At any given time, a Value
object may have become unreachable, which is to say there is no way to get to it.
Objects become unreachable when pointers change, as in these examples:
• Evaluating the definition (val x '(a b)) makes x point to a newly allocated
location. In practice, the system allocates many locations: five hold Value
objects,1 and one holds an Env link for the top‐level environment. In this
picture, the Env object is labelled env; the unlabelled objects are Values.
tl
env x • • ···
• •
a • •
b NıL
If the next definition evaluated is (set x '()), memory now looks like this:
tl
env x • • ···
NıL
a • •
b NıL
1
In order, they are 'b, '(), 'a, a cons cell, and an unspecified value associated with x, which is later
overwritten to hold a second cons cell.
The second cons cell is copied over the location containing the first. Now the
picture is
tl
env x • • ···
• •
§4.2
a • • Garbagecollection
basics
b NıL
259
The locations containing the atom a and the original copy of the second cell
of (a b) are now unreachable.
Locations holding unreachable objects are garbage. In a garbagecollected sys‐
tem, objects are allocated from a managed heap: a collection of memory blocks and
metadata structures whose mission is to satisfy allocation requests. When the heap
runs out of memory and so can no longer satisfy allocation requests, the computa‐
tion is halted while a garbage collector reclaims unreachable objects, making them
available to satisfy future requests.
Like all forms of memory management, garbage collection has overhead costs.
The overhead on allocation is bursty: most allocations are fast, but an alloca‐
tion that triggers the garbage collector makes the program pause, at least a little.
To keep the pauses short, a sophisticated collector runs frequently, in short spurts,
or even concurrently with the program that is allocating. And total overhead can be
reduced by making the heap bigger—each collection does about the same amount
of work as before, but the collector runs less often.
Garbage collection also imposes memory overhead, but here the story is more
complicated. A collector requires enough memory to hold all reachable objects, plus
an additional factor of “headroom,” so the collector doesn’t have to run too often.
How this requirement compares with manual memory management depends on
how good the manual manager is at freeing objects that are no longer needed—
a property that varies from program to program.
Garbage collection has another pleasant property—as we see in this chapter,
a garbage‐collected heap can easily be integrated into existing code; one simply
replaces malloc with a new allocator, them implements free as a no‐op.
A garbage collector scans the roots of a system and uses these roots to find reach‐
able objects. There are many variations, but they all draw from the two methods
presented in this chapter: markandsweep and copying collection.
An alternative not based on reachability is reference counting. In reference
counting, each object stores a count of the number of pointers to that object; when
the count goes to zero, the object can be put on a list of available objects. Reference
counting cannot easily reclaim cyclic garbage: an unreachable circular list has no
objects with zero counts, so it never gets reclaimed. For this reason and others,
garbage collection is normally preferable to reference counting (Section 4.8).
A managed heap has at least two components: a garbage collector and an allocator.
The garbage collector gets all the attention, but the allocator shouldn’t be over‐
looked; the two work as a team. The team serves a third actor: in the perverse
jargon of garbage collection, the client code that allocates objects and does useful
work with them is called the mutator.
The best managed heaps use multiple processor cores and multiple threads of
control to supply the mutator with new objects while disrupting its work as little
as possible (Section 4.9). But to make the basic ideas concrete, the examples in
this chapter use a much simpler model, called stop the world. The mutator runs in
4 a single thread, and it periodically allocates a new object or changes (mutates) a
pointer in an old object. (It is the accumulation of mutations that eventually makes
objects unreachable.) The allocator satisfies a typical request quickly, but when it
cannot do so, it suspends execution of the mutator entirely—“stopping the world”—
Automatic memory
and runs the garbage collector in that same thread.
management
The system in this chapter makes one other simplifying assumption: that every
260 object allocated on the heap is the same size.
4.2.1 Performance
• Collector overhead. How much additional work, per object allocated, does it
take to run the garbage collector?
• Memory overhead. How much additional memory, per object, does it take to
hold the data structures that support the managed heap?
The first two measures combine to give the cost per allocation, measured in time.
The third measure gives the space overhead of garbage collection, e.g., the ratio of
the size of the whole heap to the size of the program’s data. In garbage‐collected
systems, space can be traded for time by adjusting the size of the heap: Provided
there’s enough physical memory, enlarging the heap lowers the cost per allocation.
The size of the heap is written H .
A fourth measure is also worth keeping in mind:
• Pause time. How long does the program pause while the garbage collector is
running?
This measure matters most in programs that must interact with users or on servers
that must answer requests promptly. And it can be improved by another trade‐off:
pause times can be decreased by spending more overhead per allocation. The state
of the art keeps improving; at one time, a collector with a 50‐millisecond pause time
could claim to be “real‐time.” As I write, general‐purpose implementations of Go
and OCaml get pause times under 10 milliseconds, and “real‐time” means under
1 millisecond, if not better. Human reaction time is only about 100 milliseconds,
so 10‐millisecond pause times make garbage collection very effective in interactive
applications.
• Local variables and formal parameters, anywhere on the call stack, from
which heap‐allocated objects might be reachable
In our µScheme+ interpreter, roots are found in the same three places:
• Global variables are stored in a data structure roots.globals which has type
Env. (µScheme+ also has a species of “hidden” global variable: the list of
pending unit tests. Each list of pending unit tests is associated with a source
of definitions, and a list is stored in data structure roots.sources of type
Sourcelist.)
• Local variables and formal parameters are stored in ENV frames on the eval‐
uation stack (Chapter 3), a data structure roots.stack of type Stack.
Tricolor marking
The allocated objects, together with the pointers between them, form a graph. Fol‐
lowing pointers amounts to tracing the edges of this graph. Tracing algorithms are
modeled using an abstraction called tricolor marking. In the model, each object is
colored as follows:
The colors are abstractions; even in the simple collectors we build in this chapter,
nodes aren’t colored white, gray, or black directly. Instead, an object’s color is iden‐
tified by looking at other properties. Abstracting over the representation in this
way requires extra thinking, but it pays off because tricolor marking can guide the
implementation of many different kinds of collectors.
The easiest payoff comes from the coloring invariant: no black object ever points
to a white object; black objects point only to gray objects or to other black objects.
Another payoff comes from an abstract description of how a collector works; mark‐
and‐sweep and copying collectors use different algorithms to traverse the graph of
objects, but abstractly, both algorithms fit this description:
In a real Scheme system, all dynamically allocated objects are allocated on the man‐ 263
aged heap. In µScheme+, only objects of type Value are allocated on the managed
heap. This design is not realistic, but it keeps the interpreter simple enough to
study and modify. And the Value objects account for most memory use; objects
of types Exp, Explist, and Namelist are allocated only by the µScheme parser, so
their allocation is bounded by the amount of source code. In a real garbage collec‐
tor, the techniques we use for Values would be used for objects of all types.
Not all Value objects are allocated on the heap. In particular, intermediate values
computed by eval are either stored in the C local variable v or are stored in evalu‐
ation contexts on the µScheme+ stack (Chapter 3). As far as the garbage collector
is concerned, these objects act as roots—values reachable from the stack could be
used in future computations.
Allocating most intermediate values on the stack reduces heap allocation to just
three situations:
• When the primitive cons is applied, the locations needed to hold the car and
cdr are allocated on the managed heap, with allocate.
Any pointer to a Value allocated on the heap has to be findable during garbage
collection. Such a pointer might be embedded into other data. These data are po‐
tential roots.
The list above shows not only from which types of value a heap object might be
reachable but also which pointers to follow from such a value to reach the heap
object. Accordingly, for each type above, I have written a procedure that follows
pointers and marks heap‐allocated values. These procedures appear in Sections
4.4.2 and N.1.1.
• When it calls allocate, the mutator must tell the garbage collector about any
previously allocated location that it might still care about.
To enable the managed heap to fulfill this contract, the mutator and the heap share
information:
• The garbage collector knows enough about the mutator’s internal data struc‐
tures so it can find pointers. In our case, the garbage collector knows every
thing about the mutator’s data structures—the information about pointers is
summarized in the previous section.
The roots are
1. The global variables, which include both the user program’s variables (the
µScheme+ global environment) and any global variables internal to the mu‐
tator (the pending unit tests)
This struct is the data structure that is shared between the mutator and the garbage
collector, as a global variable:
265b. hglobal variables used in garbage collection 265bi≡ (S377d)
extern struct Roots roots;
The mutator makes sure that before any call to allocate, roots is up to date and
contains pointers to all locations that could affect the rest of the computation. The
garbage collector inspects the roots and also updates pointers to objects that it
moves. Register roots are added and removed in last‐in, first‐out order using func‐
tions pushreg and popreg.
265c. hfunction prototypes for µScheme+ 265ci≡ (S358) 265d ▷
void pushreg(Value *reg);
void popreg (Value *reg);
If the pointer passed to popreg is not equal to the pointer passed to the matching type Env 153a
type Registerlist
pushreg, it is a checked run‐time error.
S377b
The mutator may also need to push or pop all the registers on a list of values. type Stack 223
265d. hfunction prototypes for µScheme+ 265ci+≡ (S358) ◁ 265c 281a ▷ type UnitTest‐
listlist S377b
void pushregs(Valuelist regs);
type Value A
void popregs (Valuelist regs);
type Valuelist
The rest of the interface supports allocation. The managed‐heap function S309c
allocloc provides an uninitialized location; in chunk 266b, it is used to implement
allocate.
265e. hfunction prototypes for µScheme 265ei≡ (S318a S358) 266a ▷
Value *allocloc(void);
The allocator and roots structure are related by this precondition: clients may call
allocloc only when all objects that could lead to live values appear in roots. The
copying collector’s implementation of allocloc also requires that, when called, all
pointers to allocated values must be reachable from roots, so that they can be updated
when the values move.
The mutator has one more obligation: before calling allocloc, it must call
initallocate, passing a pointer to the environment that holds the global variables.
266a. hfunction prototypes for µScheme 265ei+≡ (S318a S358) ◁ 265e
void initallocate(Env *globals);
What’s new here are the calls to pushreg and popreg. When v is a cons cell and
allocloc happens to call the garbage collector, pushreg and popreg prevent the
garbage collector from reclaiming the locations pointed to by v.pair.car and
v.pair.cdr. The call to pushreg makes v a “machine register” and ensures that
the collector treats it as a root. And if the collector happens to move v’s car and
cdr, it updates v’s internal pointers to point to the new locations.
1. When an object is requested, look for a suitable object is on the free list.
If there isn’t one, ask the collector to recover some objects.
1. Mark (i.e., set the mark bit associated with) every reachable object. This
phase traverses the heap starting from the roots.
2. Sweep every object in the heap. Unmarked objects are unreachable. Place
each unreachable object on the free list, and clear the mark bit associated
with each reachable object.
If these algorithms are implemented naïvely, the sweep phase visits the entire
heap. That’s a lot of objects, and they probably don’t all fit in the cache. The mu‐
tator might pause for a long time. Or the allocator could do the sweeping. It too
must visit the entire heap, but the visit is spread out over many allocation requests.
Since the collector only has to mark, its running time drops. In this variant, called
lazy sweeping, the allocator keeps a pointer into the managed heap, advancing the
pointer one or more objects until it encounters one that can be reused. Such an
allocator is sketched below; in Exercise 8, you complete the sketch.
4.4.1 Prototype markandsweep allocator for µScheme
A mark‐and‐sweep system associates a mark bit with each heap location. To keep
things simple, I don’t try to pack mark bits densely; I just wrap each Value in an‐
other structure, which holds a single mark bit, live. By placing the Value at the
beginning, I ensure that it is safe to cast between values of type Value* and type
Mvalue*.
§4.4
267a. hprivate declarations for markandsweep collection 267ai≡ 267c ▷
Markandsweep
typedef struct Mvalue Mvalue;
collection
struct Mvalue {
Value v; 267
unsigned live:1;
};
The use of mark bits has to be announced to my debugging interface (Section 4.6.1).
267b. hms.c 267bi≡ 267e ▷
bool gc_uses_mark_bits = true;
The MValue structures are grouped into pages. A single page holds a contigu‐
ous array of objects; pages are linked together into a list, which forms the heap.
The page is the unit of heap growth; when the heap is too small, the collector calls
malloc to add one or more pages to the heap.
267c. hprivate declarations for markandsweep collection 267ai+≡ ◁ 267a 267d ▷
#define GROWTH_UNIT 24 /* increment in which the heap grows, in objects */
typedef struct Page Page;
struct Page {
Mvalue pool[GROWTH_UNIT];
Page *tl;
};
The tl field links pages into a list that is referred to by multiple pointers. Pointer
pagelist points to the head of the list, that is, the entire heap. The “heap pointer”
hp points to the next Mvalue to be allocated. And heaplimit points to the first
Mvalue after the current page, curpage.
267d. hprivate declarations for markandsweep collection 267ai+≡ ◁ 267c 268c ▷
Page *pagelist, *curpage;
Mvalue *hp, *heaplimit;
pagelist curpage
• •
hp allocloc 265e
type Env 153a
heaplimit
popreg 265c
pushreg 265c
White areas have been allocated; areas marked in gray diamonds are available for type Value A
allocation. Pages except the current one are entirely used. The number of unallo‐
cated cells in the current page is heaplimit ‑ hp.
A fresh page is made current by makecurrent.
267e. hms.c 267bi+≡ ◁ 267b 268a ▷
static void makecurrent(Page *page) {
assert(page != NULL);
curpage = page;
hp = &page‑>pool[0];
heaplimit = &page‑>pool[GROWTH_UNIT];
}
When the heap grows, it grows by one page at a time. Each new page is allocated
with calloc, so its mark bits are zeroed.
268a. hms.c 267bi+≡ ◁ 267e 269b ▷
static void addpage(void) {
It is a checked run‐time error to call addpage except when pagelist is NULL or when
curpage points to the last page in the list.
Writing the allocator is your job (Exercise 8). But I provide a prototype that does
not collect garbage; when it runs out of space, it adds a new page.
268b. hms.c [[prototype]] 268bi≡ 269c ▷
Value* allocloc(void) {
if (hp == heaplimit)
addpage();
assert(hp < heaplimit);
htell the debugging interface that &hp‑>v is about to be allocated 282ei
return &(hp++)‑>v;
}
If the heap is a directed graph containing objects of different types, the collector’s
marking phase is a depth‐first search. Starting at the roots, for each object, the
collector visits the objects it points to. When the collector visits a Value allocated
on the heap, it sets the mark bit. If it visits such a Value and the mark is already set,
it returns immediately; this test guarantees that the mark phase terminates even if
there is a cycle on the heap.
The search itself is straightforward; it uses one procedure for each type of object
to be visited. Not every type in Section 4.3.2 requires a visiting procedure, because
not every type of object is reachable. For example, an object of type Valuelist
cannot be reached by following pointers from roots.
268c. hprivate declarations for markandsweep collection 267ai+≡ ◁ 267d
static void visitloc (Value *loc);
static void visitvalue (Value v);
static void visitenv (Env env);
static void visitexp (Exp exp);
static void visitexplist (Explist es);
static void visitframe (Frame *fr);
static void visitstack (Stack s);
static void visittest (UnitTest t);
static void visittestlists (UnitTestlistlist uss);
static void visitregister (Register reg);
static void visitregisterlist (Registerlist regs);
static void visitroots (void);
To make visitenv work, I must expose the representation of environments.
(In Chapter 2, this representation is private.)
269a. hstructure definitions for µScheme+ 269ai≡ (S358)
struct Env {
Name name;
Value *loc;
Env tl;
};
§4.4
Markandsweep
Most “visit” procedures are easy to write. As an example, the visit procedure
collection
for an environment visits all of its loc pointers.
269b. hms.c 267bi+≡ ◁ 268a 269d ▷ 269
static void visitenv(Env env) {
for (; env; env = env‑>tl)
visitloc(env‑>loc);
}
The most important such procedure visits a location and sets its mark bit. Un‐
less the location has been visited already, its value is also visited.
269c. hms.c [[prototype]] 268bi+≡ ◁ 268b
static void visitloc(Value *loc) {
Mvalue *m = (Mvalue*) loc;
if (!m‑>live) {
m‑>live = 1;
visitvalue(m‑>v);
}
}
In the tricolor‐marking story, if m‑>live is not set, then m is white. Setting m‑>live
curpage 267d
makes m gray, and after m‑>v is visited, m is black. type Env 153a
A register is different from a heap location: a register has no mark bit. type Exp A
type Explist S309b
269d. hms.c 267bi+≡ ◁ 269b 269e ▷
type Frame 223
static void visitregister(Value *reg) { heaplimit 267d
visitvalue(*reg); hp 267d
} makecurrent 267e
type Mvalue 267a
Function visitvalue visits a value’s components of type Value *, Exp, and Env. type Name 43a
269e. hms.c 267bi+≡ ◁ 269d type Page 267c
static void visitvalue(Value v) { pagelist 267d
switch (v.alt) { type Register
S377b
case NIL:
type Registerlist
case BOOLV:
S377b
case NUM: type Stack 223
case SYM: type UnitTest
case PRIMITIVE: A
return; type UnitTest‐
listlist S377b
case PAIR:
type Value A
visitloc(v.pair.car);
visitloc(v.pair.cdr);
return;
case CLOSURE:
visitexp(v.closure.lambda.body);
visitenv(v.closure.env);
return;
default:
assert(0);
return;
}
assert(0);
}
The remaining visit procedures appear in the Supplement (Section N.1.1).
4.4.3 Performance
4 The cost of any single allocation can’t easily be predicted—from the source code,
you can’t tell which allocation might trigger a garbage collection or how much data
might be live at that time. Proper cost accounting requires an amortized analysis,
which considers a sequence of allocations: an entire garbage‐collection cycle. In
Automatic memory
that cycle, the garbage collector runs once, and the allocator is called N times, right
management
up to just before the next time the collector runs. In a cycle, the allocator sweeps
270 all H cells, of which L are marked live, so N = H − L.
As a simplifying assumption, suppose that the heap is in “steady state,” i.e., H ,
L, and γ do not change from one cycle to the next. Because real heaps often grow
and shrink wildly, this assumption seldom holds in practice, but it still helps predict
and compare the costs of different garbage‐collection techniques.
• Allocation cost. For mark‐and‐sweep, the cost per allocation is a constant (for
finding and returning an unmarked cell), plus some fraction of the sweeping
cost for the whole heap. On average, sweeping imposes a small cost per ob‐
ject (Exercise 14). And with high probability, the cost per object is bounded
by a small constant (Exercise 16).
• Collector overhead. The garbage collector does work proportional to the size
of the live data, not the size of the entire heap. Work per allocation is this
L
work divided by the number of allocations, so proportional to N . The work
itself is depth‐first search and setting mark bits. Setting a mark bit seems
relatively inexpensive, but the pattern of accesses to mark bits in memory
can be very irregular—so unlike sweeping, marking is not cache‐friendly.
– Every object needs a mark bit. If there is not already a bit available
in the object header, the mark bits can be pushed off into a separate
bitmap—although a bitmap militates against parallel garbage collec‐
tion.
– The mark phase must know where to find pointers in heap objects.
Ours uses the same alt field that is used to identify the type of a value,
paying no additional overhead. In real systems, the overhead is kept
low using a variety of tricks.
– A mark‐and‐sweep system requires γ > 1 to perform well. While γ can
be adjusted through a wide range, letting γ get too close to 1 results
in poor performance. The necessary headroom may be considered a
memory overhead.
– When used to allocate objects of different sizes, a mark‐and‐sweep sys‐
tem may suffer from fragmentation. That is, it may have chunks of free
memory that are too small to satisfy allocation requests. Classic analy‐
ses of fragmentation in dynamic memory allocators (Knuth 1973; Wil‐
son et al. 1995) also apply to mark‐and‐sweep allocators.
– The mark phase needs a place to store gray objects. Because our mark
phase uses recursive visiting procedures, it stores gray objects in lo‐
cal variables on the C call stack. This technique is OK if the graph of
heap objects does not contain very long paths, but a data structure, of‐
ten called a work list, stores gray objects more compactly. A work list is
usually a stack or a queue.
• Pause time. On average, allocation is fast, even when the allocator has to
sweep past marked objects. But when an allocation triggers a garbage col‐
lection, our whole system pauses long enough for the collector to mark live
data. If there is a lot of live data, such a pause may be too long for real‐time
response, but it is still better than the naïve version, which waits for the col‐ §4.5
lector to sweep the entire heap. Collectors that run in time proportional to Copying collection
the amount of live data are effective for many applications. 271
The copying method of garbage collection, also called stopandcopy, trades space
for time. A copying system uses roughly twice as much heap as a mark‐and‐sweep
system, and while the mutator is running, it leaves half the heap idle. The idle half
eventually supplies contiguous free space that can be used to satisfy future alloca‐
tion requests, making allocation blindingly fast. It works like this:
• The heap is divided into two equal semispaces, only one of which is normally
in use. That semispace, called fromspace, is itself divided into two unequal
parts: The first part contains objects that have been allocated, and the sec‐
ond contains memory that is available for allocation. The boundary between
them is marked by the heap pointer hp. Available memory is contiguous, not
fragmented into objects on a free list. Each new object is allocated from the
beginning of the available memory, by advancing the heap pointer. The heap
looks like this:
hp heaplimit
Within from‐space, on the left, the white area holds allocated objects, and
the area filled with gray diamonds is unallocated. The end of the unallo‐
cated area (and the semispace) is marked by the limit pointer heaplimit.
The striped area is not used during allocation.
Memory is allocated by incrementing hp. The white area grows and the gray‐
diamond area shrinks until all of fromspace contains allocated objects, and
there is not enough gray‐diamond area to satisfy an allocation request:
hp
heaplimit
• When the unallocated area is used up, the system switches to the other semis‐
pace, called tospace. Before the switch, the garbage collector copies all the
reachable objects from from‐space into to‐space. Because not all objects are
reachable, there is room left over in to‐space to satisfy future allocation re‐
quests. The system then “flips” the two spaces, and it continues executing
in to‐space; the allocator starts taking new memory from the first location
above the copied objects.
After a flip, the heap might look like this:
hp heaplimit
4
Automatic memory
management About 20% of objects have survived the collection, and 20% of a semispace
is 10% of the heap, so γ is about 10. In this example, because so few objects
272 have been copied relative to the number of past allocations, the copying sys‐
tem performs very well indeed.
A copying collector has something in common with the “marshallers” used to send
structured data in distributed systems; both components move data from one place
to another.2 To preserve sharing and cycles, a copying collector must copy each
live object exactly once—and once the object has been copied, the collector must
keep track of where the copy is. Luckily, the collector has a handy place to store
the information: the vacated spot from which the object was copied. The collector
uses that space to store a forwarding pointer. A forwarding pointer indicates that
an object has already been copied, and it points to the location of the copy in to‐
space. In our µScheme system, a forwarding pointer is represented by a new form
of value, with tag FORWARD.3 A second new form, INVALID, is used for debugging;
dead cells can be marked INVALID.
272a. hvalue.t 272ai≡
Lambda = (Namelist formals, Exp body)
Value = NIL
| BOOLV (bool)
| NUM (int)
| SYM (Name)
| PAIR (Value *car, Value *cdr)
| CLOSURE (Lambda lambda, Env env)
| PRIMITIVE (int tag, Primitive *function)
| FORWARD (Value *)
| INVALID (const char *)
A copying collection adjusts every pointer to every live object so that it points
into to‐space; the adjustment is called forwarding the pointer. When p points to an
object in from‐space and *p has not yet been copied, the collector copies *p to *hp.
When *p has already been copied, tag p‑>alt identifies p‑>forward as a forwarding
pointer, which the collector returns without copying *p a second time.
272b. hforward pointer p and return the result 272bi≡ (278a)
if (p‑>alt == FORWARD) {
return p‑>forward;
} else {
htell the debugging interface that hp is about to be allocated 282fi
*hp = *p;
*p = mkForward(hp); /* overwrite *p with a new forwarding pointer */
return hp++;
}
2
The copying component of a garbage collector has sometimes been used as a marshaller.
3
The FORWARD tag is not strictly necessary; a forwarding pointer can be identified simply by its value.
A pointer is a forwarding pointer if and only if it points into to‐space.
The copy operation never runs out of space because while pointers are being for‐
warded, hp points into to‐space at the boundary between allocated and unallocated
locations. Because to‐space is as big as from‐space, and because no object is copied
more than once, to‐space always has room for all the live objects.
The pointers that need to be forwarded and the objects that need to be copied
can be explained by the tricolor marking scheme.
• An object is black if it has been copied into to‐space, and the pointers it con‐
tains have been forwarded. This implies that the objects it points to have also
been copied into to‐space.
The boundary between black objects and gray objects is marked by an additional
pointer, scanp. An object’s color is determined by its address a. White objects
satisfy fromspace ≤ a < fromspace + semispacesize. Black objects satisfy
tospace ≤ a < scanp. And gray objects satisfy scanp ≤ a < hp.
All four pointers are shown in this picture:
$''0./-/ *+4$)" *'' /$*) 0.$)" # + *! .$5 ƧƥǙ 0++*. /# - - /#- -**/.ǚ
) /# # + '**&. '$& /#$.Ǜ
ë **/.Ǜ • • •
0/*(/$ ( (*-4 -*(ǰ.+ Ǜ эс енц енц эс щс щс енц эс щс
• • • • нр
()" ( )/ × â Ʈ Ʀƥ • • ã ƨƨ
ƧƬƩ
*ǰ.+ Ǜ
ģã×ďĝ
ûĝ
# - #' ǰ 3+- ..$*). - '' +-/ *! *) '$./ǜ /2* *). ''. +*$)/ /* /#
.( .4(*' âǛ
• •
× • •
• •
â тнр
**/.Ǜ • • •
-*(ǰ.+ Ǜ ыи енц енц эс щс щс енц нр эс щс
• • • • • â Ʈ Ʀƥ • • ã ƨƨ
*ǰ.+ Ǜ эс
×
ģã×ďĝ
ûĝ
# ƞ-./ *% / $) !-*(ǰ.+ #. ) *+$ $)/* /*ǰ.+ ) - +' 2$/#
!*-2-$)" +*$)/ -ǚ 2#$# $. .#*2) 2$/# *// '$) Ǚ # -**/ )*2 +*$)/. /* /#
*+4ǚ 2$/# /# +*$)/ - +..$)" Ǩ #$)ǩ !-*(ǰ.+ .* . )*/ /* '0// - /# $"-(Ǚ
# *+$ *% /ǚ . .#*2) 4 $/. /#$& *- -ǚ $. )*2 "-4Ǚ Ǹ% /. $) /# /*+ -*2
- 2#$/ Ǚǹ
Ơ - '' /#- -**/. #1 ) !*-2- ǚ /#/ $.ǚ Ơ - /# 3 0/$*) *! /# ƞ-./
'**+ $) *+4 '' - #' *% /. $)/* /*IJ.+ ƧƬƨǚ /# # + '**&. '$& /#$.Ǜ
**/.Ǜ • • •
-*(ǰ.+ Ǜ ыи ыи енц ыи щс щс енц нр эс щс
• • • • • Ʈ Ʀƥ • • ã ƨƨ
ŕëěì
эс эс енц *+4$)" *'' /$*)
*ǰ.+ Ǜ
× â • • ƧƬƪ
ģã×ďĝ
ûĝ
*2 /# *'' /*- ./-/. .))$)" /# "-4 *% /. '*/ /2 ) ģã×ďĝ ) ûĝǙ
-4 *% /. +*$)/ /* 2#$/ *% /.ǚ 2#$# $) *+4$)" *'' /*- ( ). /#/ /#
$)/ -)' +*$)/ -. $) /# . *% /. +*$)/ & /* !-*(ǰ.+ Ǚ
))$)" /# ƞ-./ /2* "-4 *% /. * . )*/ #)" /# # +ǚ 0. /# . *ǰ
% /. #1 )* $)/ -)' +*$)/ -.Ǚ Ǹ0/ *) ģã×ďĝ (*1 . +./ /# (ǚ /# ƞ-./ /2* "-4
*% /. - *).$ - '&Ǚǹ ))$)" /# /#$- "-4 *% / Ǹ/# +$-ǹ !*-2-. $/.
/2* $)/ -)' +*$)/ -.Ǚ # .4(*' Ǣ× #. '- 4 ) *+$ $)/* /*ǰ.+ ǚ .* !*-ǰ
2-$)" /# ã×ğ * .)ǧ/ *+4 )4 /ǜ $/ %0./ %0./. +*$)/ -Ǜ
**/.Ǜ • • •
-*(ǰ.+ Ǜ ыи ыи енц ыи щс щс енц нр эс щс
• • • • • Ʈ Ʀƥ • • ã ƨƨ
ģã×ďĝ
ûĝ
**/.Ǜ • • •
-*(ǰ.+ Ǜ ыи ыи ыи ыи щс щс енц нр эс щс
• • • • Ʈ Ʀƥ • • ã ƨƨ
ģã×ďĝ
ûĝ
# *'' /*- *)/$)0 . *+4$)" *% /. +*$)/ /* 4 ǯģã×ďĝ 0)/$' 1 )/0''4
ģã×ďĝ /# . 0+ 2$/# ûĝǙ *2 /*ǰ.+ #*'. *)'4 '& *% /. ) !-*(ǰ.+
#*'. *)'4 2#$/ *% /.Ǚ -*(ǰ.+ ) $.- ǚ ) /# (0//*- ) - ǰ
.0( 3 0/$*)Ǚ /. ) 3/ ''*/$*) - ,0 ./. 2$'' ./$.ƞ 0.$)" /# !*0- '*ǰ
/$*). - *1 - $) /*ǰ.+ Ǚ
ë **/.Ǜ • • •
-*(ǰ.+ Ǜ ыи ыи ыи ыи щс щс ыи ыи эс щс
0/*(/$ ( (*-4 • • • • Ʈ Ʀƥ • • ã ƨƨ
()" ( )/
ģã×ďĝ
ûĝ
''*/$*)
# ''*/*- / ./. !*- # + 3#0./$*)ǚ $)- ( )/. ûĝǚ ) - /0-). /# +-$*- 1'0
*! ûĝǙ ) - ' .4./ (.ǚ ûĝ $. & +/ $) - "$./ - ) ×ĊĊĔãĊĔã $. $)'$) Ǚ
ƧƬƫǙ *+4ě ƧƬƫ≡ ƧƬƬ
f×Ċĭëǯ ×ĊĊĔãĊĔãǙĸĔýéǚ ǝ
ýõ Ǚûĝ ȓȓ ûë×ĝĊýĎýĩǚ
ãĔĊĊëãĩǙǚǵ
×ģģëğĩǙûĝ Ȕ ûë×ĝĊýĎýĩǚǵ
/ '' /# 0""$)" $)/ -! /#/ ûĝ $. *0/ /* ''*/ ƧƭƧ!
ğëĩĭğď ûĝȎȎǵ
Ǟ
-$)" -**/.
0./ . /# (-&ǰ)ǰ.2 + .4./ ( #. 1$.$/$)" +-* 0- !*- # /4+ *! +*/ )ǰ
/$' -**/ǚ /# *+4$)" .4./ ( #. .))$)" +-* 0- !*- # /4+ *! +*/ )/$'
root. These procedures implement the chunks of the form hscan…, forwarding all
internal pointersi in chunk 273.
277a. hprivate declarations for copying collection 276ai+≡ ◁ 276b 278b ▷
static void scanenv (Env env);
static void scanexp (Exp exp);
static void scanexplist (Explist es);
static void scanframe (Frame *fr);
static void scantest (UnitTest t);
static void scantests (UnitTestlist ts); §4.5
static void scanloc (Value *vp); Copying collection
The implementations of the scanning procedures are more complicated than 277
they would be in a real system. In a real system, scanning procedures would simply
forward internal pointers. In our system, because only Value objects are allocated
on the heap, scanning procedures forward pointers to Value objects but traverse
pointers to other types of objects. For example, to scan an environment, the col‐
lector forwards the loc pointer and traverses the tl pointer (by advancing env).
277b. hcopy.c 276ci+≡ ◁ 276c 277c ▷
static void scanenv(Env env) {
for (; env; env = env‑>tl)
env‑>loc = forward(env‑>loc);
}
The code that scans an object forwards the pointers of type Value * but tra‐
verses the pointers of types Exp and Env.
277c. hcopy.c 276ci+≡ ◁ 277b 278a ▷
static void scanloc(Value *vp) {
switch (vp‑>alt) {
case NIL:
case BOOLV:
case NUM:
case SYM:
return;
case PAIR:
vp‑>pair.car = forward(vp‑>pair.car);
vp‑>pair.cdr = forward(vp‑>pair.cdr);
return;
case CLOSURE: collect S377e
scanexp(vp‑>closure.lambda.body); type Env 153a
type Exp A
scanenv(vp‑>closure.env);
type Explist S309b
return; forward 278b
case PRIMITIVE: type Frame 223
return; type UnitTest
default: A
assert(0); type UnitTestlist
S309b
return;
type Value A
}
}
Forwarding
The scanning procedures above closely resemble the visiting procedures used by
a mark‐and sweep collector (Sections 4.4.2 and N.1.1). One difference is that the
forward operation, as shown in chunk hforward pointer p and return the result 272bi,
never makes a recursive call.
The complete implementation of forward suffers from one more subtlety,
which arises because a root can appear on the context stack more than once. For ex‐
ample, an evaluation stack might contain two ENV frames whose environments
share a Value * pointer associated with the name foldr. When the second such
4 frame is scanned, the loc field associated with foldr already points into to‐space.
Such a pointer must not be forwarded.
278a. hcopy.c 276ci+≡ ◁ 277c
static Value* forward(Value *p) {
Automatic memory
if (isinspace(p, tospace)) {
management
/* already in to space; must belong to scanned root */
278 return p;
} else {
assert(isinspace(p, fromspace));
hforward pointer p and return the result 272bi
}
}
Heap growth
If implemented carelessly, this strategy would copy the live data twice when the
heap grows. A careful implementation delays the growth of the heap until the next
collection (Exercise 4). Or the issue can be eliminated entirely by splitting each
semispace into pages (Exercise 5).
4.5.4 Performance
A cons cell is allocated and initialized using just a load, a test, three stores,
a move, and an add.
When a system must support objects of varying sizes, a copying allocator
performs well without any additional data structures or adaptations. Unlike
a mark‐and‐sweep allocator, a copying allocator works the same way regard‐
less of how many bytes are requested. It does not need multiple free lists,
first‐fit search, or any other strategy to find a chunk of free memory of an
appropriate size. It simply tests and increments.
• Memory overhead. Our copying system pays only two memory overheads:
A copying system has no memory overhead for mark bits. And because our
system stores the gray objects on the heap itself, in the style of Cheney (1970),
it requires no stack or work list.
• Pause time. Copying allocation always takes constant time; the only pauses
are at collections. These pauses take time proportional to the amount of live
data.
4.6 DEBUGGıNG A COLLECTOR
4 • The system can fail to recycle an unreachable object. Such a fault is a memory leak.
A memory leak does not affect correctness, only performance: it makes the
garbage collector run more often than it should, and it makes the heap grow
faster than it should. Since we rarely know how often a collector “should” run
Automatic memory or how fast a heap “should” grow, these symptoms are hard to spot—a slow
management memory leak may go unnoticed for a long time. A memory leak is easiest to
spot when it makes the heap grow so fast that memory is exhausted.
280
• The system can recycle an object that is still reachable. When such an object is
reused, its contents change. From the perspective of an old pointer to the
object, the change happens for no reason, at an unpredictable time. Such a
fault, which could occur if a root is overlooked or if a tracing procedure fails
to follow a pointer, can be difficult to detect.
Memory leaks aren’t catastrophic. You can detect them with a tool like Valgrind
or Pin (Section 4.10.2), which instruments the code and looks for “lost” memory.
If you don’t have such a tool, you might still find a leak by observing that the garbage
collector is retaining too much live data. To know how much is too much, you might
rely on regression testing or on careful analysis. Once you’re sure there’s a leak,
you (or a tool) can examine the heap to find out exactly which objects are not being
recycled and by what combination of pointers those objects are reachable.
Premature recycling is harder to detect. It usually occurs because the muta‐
tor has kept a root (a pointer to a heap object) and hasn’t told garbage collector.
You might detect such a problem by means of aggressive assertions, also using a
tool like Valgrind or Pin—this time to flag reads from or writes to an object that has
been reclaimed but not yet reused—or by one of the techniques below. That said,
you probably won’t have such problems while working the Exercises, because the
code that keeps roots up to date has been tested extensively.
If you need to find memory errors, use my debugging code. As described below,
it tracks the three states shown in Figure 4.1.
• Memory that is owned by the collector should be read or written only by col‐
lector code.
• Memory that is owned by the mutator may be read or written freely. The
collector should read or write it only during a call to allocloc.
release acquire
Owned by heap
jects as INVALID, and when Valgrind is available, they get it to enforce these rules:
• No code should read or write any object owned by the collector. If the col‐
lector mistakenly reclaims a reachable object, then the next time any code
reads or writes that object, Valgrind will complain.
This function must be used carefully; the copying collector can announce the ac‐
quisition of an entire block at once, but because the mark‐and‐sweep collector
wraps each Value in an Mvalue, it must call gc_debug_post_acquire on one ob‐
ject at a time.
When a block of memory that belongs to the collector is no longer needed and
is about to be released, the collector should call gc_debug_pre_release just before
calling free. As with acquisition, the mark‐and‐sweep collector must release one
object at a time.
281b. hfunction prototypes for µScheme+ 265ci+≡ (S358) ◁ 281a 281c ▷
void gc_debug_pre_release(Value *mem, unsigned nvalues);
Just before the allocator delivers a heap object to the mutator, it should call
gc_debug_pre_allocate.
281c. hfunction prototypes for µScheme+ 265ci+≡ (S358) ◁ 281b 281d ▷ type Value A
void gc_debug_pre_allocate(Value *mem);
Some of the debugging functions are used in some of the prototype code above:
282d. htell the debugging interface that each object on page has been acquired 282di≡ (268a)
{ unsigned i;
for (i = 0; i < sizeof(page‑>pool)/sizeof(page‑>pool[0]); i++)
gc_debug_post_acquire(&page‑>pool[i].v, 1);
}
282e. htell the debugging interface that &hp‑>v is about to be allocated 282ei≡ (268b)
gc_debug_pre_allocate(&hp‑>v);
282f. htell the debugging interface that hp is about to be allocated 282fi≡ (272b 276c)
gc_debug_pre_allocate(hp);
• Trigger a garbage collection before every allocation request. This trick may
slow your program by many orders of magnitude, but it gives you a chance
of detecting a missing root as soon as it disappears.
• After each collection, check that every pointer points to a valid object in the
right space.
• After each collection, take a snapshot of the heap and run the collector
again—once for mark‐and‐sweep, twice for copying. If the new heap is not
identical to the snapshot, there is a bug in the collector. (This technique will
not detect a missing root.)
To debug a mark‐and‐sweep collector, try the following:
• After marking the live objects, sweep the entire heap, making unmarked ob‐
jects INVALID.
• If you are worried about memory corruption in your mark bits, use a whole
word instead of a single bit. Instead of 0 and 1, use a pair of unusual values
like 0xdeadbeef and 0xbadface. If you ever see a different value, something §4.7
is stomping on your mark bits. Markcompact
collection
To debug a copying collector, try the following:
283
• When you “free” old spaces, don’t actually call free. Invalidate them, and let
validate flag stale pointers to those spaces.
The great advantage of copying collection is that by moving live objects into to‐
space, it creates contiguous free space for allocation. Contiguous free space is also
created by markcompact collection. Like a mark‐sweep collector, a mark‐compact
collector marks live objects, but instead of sweeping reclaimed objects onto a free
list, it moves the live objects to eliminate the gaps between them, leaving free space
contiguous. Live objects are often moved using a “sliding” algorithm, which pre‐
serves the order of live objects in memory. This algorithm maintains locality prop‐
erties that can affect the performance of the mutator.
Mark‐compact collection offers the same benefits for allocation as a copying
collector: fast allocation from a contiguous block of memory. And it takes less
space. But because it may touch every live object twice—once to mark it and once gc_debug_post_
acquire 281a
to compact it, it takes more time than either copying or mark‐sweep collection.
gc_debug_pre_
allocate 281c
4.8 REFERENCE COUNTıNG hp,
in µScheme+
(copying)
Tracking roots appears to require support from the compiler or some run‐time 276b
overhead.4 If you can’t get support from the compiler and you don’t want to pay in µScheme+
run‐time overhead, you might look for other ideas. Because an object is definitely (mark‐sweep)
267d
unreachable if there are no references to it, one idea is to reuse an object once it is
page 268a
no longer referred to by any other object. type Value A
To make the idea work, a system needs to track not simply whether an object
is referred to, but how many times. For example, after the following definitions are
evaluated, the Value object for 'a is referred to twice, from both x and y:
283. htranscript 258i+≡ ◁ 258
‑> (set x '(a b))
‑> (set y (car x))
• When a µScheme procedure exited, the interpreter would decrement the ref‐
erence counts of the procedure’s formal parameters—even if the exit were
caused by a run‐time error.
• When the interpreter left the body of a let expression, it would decrement
the reference counts of let‐bound objects.
• When the interpreter allocated a new cell is created, it would increment the
reference counts of the car and cdr.
• When a decrement made the reference count of a cons cell go to zero, the
interpreter would put the cons cell on a free list.
• When the interpreter took a cons cell off the free list, it would decrement the
counts of the car and cdr.
• The compiler identifies the global variables that point to heap objects.
• The compiler identifies the size and layout of each heap object, so the collec‐
tor will know how big the object is and what parts of it point to other heap
objects. Most compilers put one or more header words before the heap ob‐
ject. A header word might point to a map that shows where the pointers are
in the object, or it might point to a snippet of bytecode that the collector can
interpret to find pointers within the object.
In some systems, the header simply tells how many words there are in the
object, and the compiler uses the low bit of each word as a marker to dis‐
tinguish pointers from integers. Such systems are easy to identify, as they
provide, e.g., only 63 bits of integer precision on a 64‐bit machine.
• The mutator could update a pointer inside a black object, which by definition
has already been marked live or copied and is not slated to be revisited.
This issue is resolved by a write barrier. A write barrier forces the mutator to notify
the collector when it writes a pointer into an object allocated on the heap. When
the collector learns that a black object has been mutated to contain a pointer to
a white or gray object, it has a choice: it may follow the new pointer right away,
making it point to a gray object, or it may recolor the black object gray, scheduling
it to be traced again before collection completes.
When notified by a write barrier, the garbage collector saves the location that
is written to, in a data structure called a remembered set. A remembered set may or
may not be precise; precision can be expensive. For example, the collector might
choose to log every pointer that is updated via a write barrier, giving it very precise
information—but the log may take significant space. Or at another extreme, the
collector might simply mark a page containing the object written to, costing only
one bit of space overhead—but it may eventually need to scan the entire page for
interesting pointers.
Beyond generational, concurrent, and parallel collection, many other tech‐
niques are available to deploy.
• Modern collectors can work even without support from the compiler. The
mark‐and‐sweep collector developed by Boehm and Weiser (1988) works
without any compiler support. It finds roots by examining every word on
the call stack, plus the program segments that hold initialized and uninitial‐
ized data. It finds pointers by examining every word in every heap‐allocated
object. In both cases, if a word appears to point to a heap‐allocated object,
the collector conservatively assumes that it is a pointer, and therefore that
the object pointed to is live. (Because it is in cahoots with the allocator, the
garbage collector knows the addresses of all the heap‐allocated objects.) This
collector has achieved remarkable results with C and C++ programs, some‐
times outperforming malloc and free.
4.10 SUMMARY
EVALUATıON ſTACĸ In µScheme+, the stack S that holds all the information in a
CALL ſTACĸ, plus additional information about what computation takes place
after the current expression is evaluated. §4.10
Summary
EXPLıCıT MEMORY MANAGEMENT A memory‐management technique in which the
HEAP OBȷECTſ must be recovered and reused by the programmer, using ex‐ 289
plicit primitives to allocate and deallocate objects. MEMORY ſAFETY is the
programmer’s problem. Compare AUTOMATıC MEMORY MANAGEMENT.
FORWARDıNG POıNTER A value left behind after a HEAP OBȷECT has been copied
by a COPYıNG COLLECTOR, so that the object won’t be copied a second time.
GARBAGE COLLECTıON An algorithm that examines the HEAP, finds the LıVE DATA,
recovers all HEAP OBȷECTſ that are not live data, and uses their memory to
satisfy future requests for HEAP ALLOCATıON. Garbage collection may be
done while the rest of the program is stopped, as in this chapter. It may
be done incrementally, so that garbage collection is interleaved with other
computation. And it may be done concurrently, with both collector and MU‐
TATOR doing work at the same time. Typical methods include MARĸ‐AND‐
ſWEEP COLLECTıON and COPYıNG COLLECTıON.
GENERATıONAL COLLECTOR A collector that divides HEAP OBȷECTſ into different
groups, called generations, according to the number of collections they have
survived. Objects that have survived more collections are collected less fre‐
quently.
GENERATıONAL HYPOTHEſıſ The hypothesis that most objects die young. (In the
literature, this hypothesis is called the weak generational hypothesis.)
HEAP Memory area used for objects that may outlive the activation of the function
that allocates them. In languages with EXPLıCıT MEMORY MANAGEMENT,
the heap is managed by the programmer using primitives like C’s malloc and
free or C++’s new and delete. “Heap” also means the set of all objects allo‐
cated in the heap, which is modeled by the HEAP GRAPH.
LıVE OBȷECT A HEAP OBȷECT whose contents might affect a future computation.
Liveness is an undecidable property, so it is approximated by REACHABıLıTY:
an object that is REACHABLE from a ROOT is deemed live. Compare DEAD
OBȷECT.
STACĸ ALLOCATıON Allocation on the CALL ſTACĸ. Used for variables and objects
that are guaranteed to be DEAD when a function returns. In Impcore and C,
formal parameters and local variables die on return and so can be allocated
on the stack. In Scheme, a formal parameter or local variable can live on in
a closure; if it is not captured in any closure, it can be allocated on the stack.
Formals and locals that are captured in a closure are usually allocated on the
HEAP, unless a sophisticated analysis shows that stack allocation is safe.
4 1
2 to 6
4.5
4.5
Complete the copying collector.
Measure, analyze, or extend your completed copying col‐
lector.
7 and 8 4.4 Complete the mark‐and‐sweep collector.
Automatic memory
management 9 to 11 4.4 Measure, analyze, or extend your completed mark‐and‐
sweep collector.
292 12 4.4 Prove that lazy sweeping works.
13 4.5 Analyze costs of copying collection.
14 to 16 4.4 Analyze costs of mark‐and‐sweep collection.
17 to 19 4.4 or 4.5 Experimental study of object lifetimes. Requires a collec‐
tor.
4.11 EXERCıſEſ
The exercises are summarized in Table 4.2. As highlights, I recommend that you
undertake these exercises:
• Instrument your collector to gather statistics, and measure work per alloca‐
tion as a function of the ratio of heap size to live data (Exercises 2 and 3).
• Derive a formula for the amount of work per allocation involved in copying
collection, again as a function of the ratio of heap size to live data. Then
compare the results from your measurements with the results predicted by
your formula (Exercise 13). Once you understand the algorithms, you can
predict costs very accurately.
The copying collector is a little easier to build, and the mark‐and‐sweep collector
is a little easier to analyze.
A. After the expression (set xs (cdr xs)) is evaluated, what object or objects are
likely to become garbage, and why?
B. Conceptually, what is a root?
C. What are the three categories of root?
D. What is the mutator?
E. Copying an object seems like it would always be more expensive than marking.
But the copying collector has a compensating performance benefit. What is it?
F. If a managed heap is just barely larger than the amount of live data, what goes
wrong?
G. If all the objects we are trying to reclaim have type Value, what is the point in
looking at objects of type Env?
H. The mark phase is complete and an object is unmarked. What color is it: black, §4.11
white, or gray? Exercises
I. The mark phase is complete and an object is marked. What color is it: black,
293
white, or gray?
J. The mark phase is in progress, and an object is marked. What two colors might
it be?
K. In copying collection, what is a forwarding pointer?
L. During copying collection, an object has been copied into to‐space, but objects
that it points to are still in from‐space. What color is it: black, white, or gray?
M. During copying collection, what color are the objects in from‐space?
The first four exercises are cumulative; each builds on the ones before it. The last
two exercises build on just the first.
(a) Write a copy procedure that copies all live objects into to‐space and
leaves hp and heaplimit pointing to appropriate locations in to‐space.
(b) Write a function collect to be called from allocloc in chunk 276c.
This function should call copy, flip the semispaces, and possibly en‐
large the heap.
(a) During every collection, record the number of live objects copied. Af‐
ter the collection, have your copy procedure print the total number of
locations on the µScheme heap (the heap size), the number that hold
live objects at the given collection (the live data), and the ratio of the
heap size to live data.
(b) Gather and print statistics about total memory usage: after every 10th
garbage collection, and also when your interpreter exits, print the total
number of cells allocated and the current size of the heap. This infor‐
mation will give you a feel for the power of reuse; with garbage collec‐
tion, some of the programs in this book can run in 60 times less memory
than without garbage collection.
(c) When your interpreter exits, print the total number of collections and
the number of objects copied during those collections.
3. Implement a γ based policy for heap growth. The performance of a garbage col‐
lector is affected by the size of the heap, which should be controllable by the
programmer. My function gammadesired, which is defined in chunk S369a,
makes it possible to use the µScheme variable &gamma‑desired to control the
size of the heap. Because µScheme doesn’t have floating‐point support, the
integer &gamma‑desired represents 100 times the desired ratio of heap size to
live data. Using gammadesired, modify collect so that after each collection
1
it enlarges the heap until the actual γ is at least 100 &gamma‑desired.
4 (a) Measure the amount of work done by the collector for different values of
&gamma‑desired and for different programs. Think carefully about the
units in which work is measured.
Automatic memory
(b) Plot a graph that shows collector work per allocation as a function of the
management
value of &gamma‑desired.
294 (c) Using your measurements from part (a), choose a sensible default
value for γ . Use that default when the µScheme code has not set
&gamma‑desired.
Remember that γ is the ratio of the total heap size to the amount of live data,
not the ratio of the size of a semi‐space to the amount of live data.
4. Reduce total work by delaying heap growth. Enlarging the heap immediately af‐
ter a copy operation requires a second copy operation to get the live data into
the new heap. This second copy costs work without recovering new mem‐
ory; it is pure overhead. Change collect so that if the heap needs to be en‐
larged, it delays the job until just before the next collection. Measure the dif‐
ference in GC work per allocation for both short‐running and long‐running
programs.
You can read more about such a system in the paper by Marlow et al. (2008).
The first four exercises are cumulative; each builds on the ones before it.
• The allocator must implement not only allocation but also the unmark
and sweep phases of collection. It will sweep through the heap from the
first page to the last page. Instead of just taking the location pointed to
by hp, as in chunk 268b, the allocator must check to see if the location
is marked live. If marked, it was live at the last collection, so it cannot §4.11
be used to satisfy the allocation request. Skip past it and mark it not Exercises
live. 295
When the allocator finds an unmarked object, it sweeps past the object
and returns it to satisfy the allocation request, just as in chunk 268b.
If the allocator reaches the end of the heap without finding an un‐
marked object, it calls mark.
• In the new system, end‐of‐page is not the same as end‐of‐heap. For
example, after some allocation requests, the heap might look like this:
pagelist curpage
• •
hp
heaplimit
White areas, to the left of the heap pointer, have been used to satisfy
previous allocation requests. Areas marked with gray diamonds, to the
right of the heap pointer, are potentially available to satisfy future allo‐
cation requests.5 When hp reaches heaplimit, the allocator must look
at the next page; modify the code in chunk 268b to make it so. Only
after there are no more pages may the allocator call mark.
• After your allocator calls mark, have it call makecurrent(pagelist),
which will reset hp, heaplimit, and curpage to point into the first page.
• The allocator now guarantees that when mark is called, the entire heap
is unmarked (Exercise 12). But mark cannot guarantee to recover any‐
thing; every cell on the heap might be live. The allocator may have to
enlarge the heap by adding a new page. Write a procedure growheap
for this purpose.
You may find it helpful to split allocloc into two functions: one that attempts
to allocate without calling mark, but sometimes fails, and another that may
call the first function, mark, and growheap.
You will have an easier time with your implementation if you work Exer‐
cise 12 first—a thorough understanding of the invariants makes the imple‐
mentation relatively easy.
(a) During every collection, record the number of objects marked. After
the collection, have your mark procedure print the total number of lo‐
5
They are only “potentially” available because if they are marked, they were live at the last collection
and must be assumed to be live now. The allocator must unmark and skip over such cells.
cations on the µScheme heap (the heap size), the number that hold live
objects at the given collection (the live data), and the ratio of the heap
size to live data.
4 (b) Gather and print statistics about total memory usage: after every 10th
garbage collection, and also when your interpreter exits, print the total
number of cells allocated and the current size of the heap. This infor‐
mation will give you a feel for the power of reuse; with garbage collec‐
Automatic memory tion, some of the programs in this book can run in 60 times less memory
management than without garbage collection.
(c) When your interpreter exits, print the total number of collections and
296
the number of objects marked during those collections.
10. Implement a γ based policy for heap growth. The performance of a garbage col‐
lector is affected by the size of the heap, which should be controllable by the
programmer. My function gammadesired, which is defined in chunk S369a,
makes it possible to use the µScheme variable &gamma‑desired to control the
size of the heap. Because µScheme doesn’t have floating‐point support, the
integer &gamma‑desired represents 100 times the desired ratio of heap size
to live data.
(a) Modify your collector so that after each collection it enlarges the heap,
possibly by adding more than one page, until the measured γ is as close
1
as possible to 100 &gamma‑desired, without going under. For example,
executing (set &gamma‑desired 175) should cause your collector to in‐
crease the heap size to make γ about 1.75. (If γ is too big, do not try to
make the heap smaller; see Exercise 15.)
(b) Measure the amount of work done by the collector for different values of
&gamma‑desired and for different programs. Think carefully about the
units in which work is measured.
(c) Plot a graph that shows collector work per allocation as a function of the
value of &gamma‑desired.
(d) Using your measurements from part (b), choose a sensible default
value for γ . Use that default when the µScheme code has not set
&gamma‑desired.
11. Placement of mark bits. Our system puts mark bits in the object headers even
though there’s no spare bit and we therefore have to allocate an extra word
per object.
(a) Rewrite the allocator and collector to pack the mark bits into one or
more words in the page. You will need to be able to map the address
of an object to the address of the page containing that object. If you
arrange for the addresses of pages to fall on k ‐bit boundaries, where
GROWTH_UNIT < 2k , you can find the address of the page simply by
masking out the least significant k bits of the address of the object.
(b) Using the new representation, how many more objects can fit into an
page of the same size? What does this result imply about choosing γ ?
(c) Will this representation work well for a parallel collector? Why or why
not?
12. Correctness of lazy sweeping. Prove that in the scheme outlined in Exercise 8,
mark is called only when no S‐expression is marked live.
(a) Find an invariant for the current page pointed to by curpage. When
&curpage‑>pool[i] < hp, you’ll need a property for curpage‑>pool[i];
when &curpage‑>pool[i] >= hp, you’ll need a different property. Your
growheap procedure will have to establish the invariant for every new
page.
§4.11
(b) Extend the invariant to include pages before and after curpage. The
Exercises
invariants for these two kinds of pages should look an awful lot like
the invariants for curpage‑>pool[i] where &curpage‑>pool[i] < hp 297
and &curpage‑>pool[i] >= hp. Together, these invariants describe the
white and gray‐diamond areas in the picture of the heap.
(c) Show that growheap, allocloc, and mark all maintain this invariant.
(d) Show that conditions when mark is called, together with the invariant,
imply that mark is called only when no S‐expression is marked live.
13. Cost of copying collection as a function of γ . Even a simple analysis can enable
you to predict the cost of garbage collection.
17. Distribution of objects’ lifetimes. Pick a collector and measure the distributions
of objects’ lifetimes for several programs. Measure lifetimes in units of num
ber of objects allocated. You will need to add a field to each object that tells
when it was allocated, and at each collection you will be able to estimate the
lifetimes of the objects that died at that collection. The fewer objects that are
allocated between collections, the more accurate your estimate will be.
18. Measuring space lost to drag. An object may becomes unreachable only some
times after it is last used. This time, called the drag time of the object,
contributes to excess space usage in garbage‐collected systems. Drag time
should be measured in units of number of objects allocated. Measure drag
times in your own system:
(a) Keep count of the total number of objects ever allocated. This count
will act as a clock.
(b) Add a field to each object that tells when it was last used. Every time
an object is used, its field should be set to the current time. An object
might be considered “used” when it is looked up in an environment or
is obtained by a car or cdr operation.
(c) Add another field to each object that tells when it was last known to be
reachable. This field can be updated by the garbage collector, or for
more accurate measurements, you can create a checking function that
will run more frequently and will update the fields. Such a checking
function would look a lot like mark.
(d) When an unreachable object is garbage‐collected, log the two times
(time of last use and time of unreachability) to a file. When the pro‐
gram ends, log the same information for the remaining objects.
(e) Write a program to read the log and compute two curves. The inuse
curve records the number of objects in use as a function of time. The
reachable curve records the number of objects that are reachable, also
as a function of time.
(f) The area under each curve is a good measure of space usage. The ratio
of in‐use space to reachable space can measure the degree to which
reachability overestimates the lifetimes of objects. Compute this ratio
for several executions of several programs.
301
Helpful properties of the ML family of languages
• ML is safe: there are no unchecked run‐time errors, which means there are
In my C code, Name is an abstract type, and by design, two values of type Name can
be compared using C’s built‐in == operator. In my ML code, because ML strings
are immutable and can be meaningfully compared using ML’s built‐in = operator,
names are represented as strings.
303. hsupport for names and environments 303i≡ (S213a) 304 ▷
type name = string
ML’s type syntax is like C’s typedef; it defines a type by type abbreviation.
Each µScheme name is bound to a location that contains a value. In C, such a
location is represented by a pointer of C type Value *. In ML, such a pointer has
type value ref. Like a C pointer, an ML ref can be read from and written to, but
unlike a C pointer, it can’t be added to or subtracted from.
Table 5.1: Correspondence between µScheme semantics and ML code
5 d
e
Definition
Expression
def (page 307)
exp (page 306)
x Name name (page 303)
In C, the code that looks up or binds a name has to know what kind of thing a
name stands for; that’s why the Impcore interpreter uses one set of environment
functions for value environments ξ and ρ and another set for a function environ‐
ment ϕ. In ML, the code that looks up or binds a name is independent of what
a name stands for; it is naturally polymorphic. One set of polymorphic functions
suffices to implement environments that hold locations, values, or types.
ML has a static type system, and polymorphism is reflected in the types. An en‐
vironment has type 'a env; such an environment binds each name in its domain
to a value of type 'a. The 'a is called a type parameter or type variable; it stands for
an unknown type. (Type parameters are explained in detail in Section 6.6, where
they have an entire language devoted to them.) Type 'a env, like any type that takes
a type parameter, can be instantiated at any type; instantiation substitutes a known
type for every occurrence of 'a. µScheme’s environment binds each name to a mu‐
table location, and it is obtained by instantiating type 'a env using 'a = value ref;
the resulting type is value ref env.
My environments are implemented using ML’s native support for lists and pairs.
Although my C code represents an environment as a pair of lists, in ML, it’s easier
and simpler to use a list of pairs. The type of the list is (name * 'a) list; the type
of a single pair is name * 'a. A pair is created by an ML expression of the form
(e1 , e2 ); this pair contains the value of e1 and the value of e2 . The pair (e1 , e2 )
has type name * 'a if e1 has type name and e2 has type 'a.
304. hsupport for names and environments 303i+≡ (S213a) ◁ 303 305a ▷
type 'a env = (name * 'a) list
The fun definition form is ML’s analog to define, but unlike µScheme’s define,
it uses multiple clauses with pattern matching. Each clause is like an algebraic law.
The first clause says that calling find with an empty environment raises an excep‐
tion; the second clause handles a nonempty environment. The infix :: is ML’s way
of writing cons, and it is pronounced “cons.”
To check x ∈ dom ρ, the ML code uses function isbound.
305c. hsupport for names and environments 303i+≡ (S213a) ◁ 305b 305d ▷
fun isbound (name, []) = false
| isbound (name, (x, v)::tail) = name = x orelse isbound (name, tail)
Again using ::, function bind adds a new binding to an existing environment.
Unlike Chapter 2’s bind, it does not allocate a mutable reference cell.
305d. hsupport for names and environments 303i+≡ (S213a) ◁ 305c 305e ▷
fun bind (name, v, rho) = bind : name * 'a * 'a env ‑> 'a env
(name, v) :: rho
Even though an 'a env is a list of pairs, functions that operate on two lists, like
those in Chapters 1 and 2, are still useful. Function bindList adds a sequence of
bindings to an environment; it is used to implement µScheme’s let and lambda.
If the lists aren’t the same length, it raises another exception. Function bindList
resembles Chapter 2’s bindalloclist, but it does not allocate. Related function
mkEnv manufactures a new environment given just a list of names and 'a’s.
305e. hsupport for names and environments 303i+≡ (S213a) ◁ 305d 305f ▷
bindList : name list * 'a list * 'a env ‑> 'a env
exception BindListLength mkEnv : name list * 'a list ‑> 'a env
fun bindList (x::vars, v::vals, rho) = bindList (vars, vals, bind (x, v, rho))
| bindList ([], [], rho) = rho
| bindList _ = raise BindListLength
type name 303
fun mkEnv (xs, vs) = bindList (xs, vs, emptyEnv)
5 NotFound
BindListLength
A name was looked up in an environment but not found there.
A call to bindList tried to extend an environment, but it passed
two lists (names and values) of different lengths (also raised by
mkEnv).
Interlude:
µScheme in ML RuntimeError Something else went wrong during evaluation, i.e., during the
execution of eval.
306
• None of the fields of exp, value, or lambda is named. Instead of being re‐
ferred to by name, these fields are referred to by pattern matching.
A primitive function that goes wrong raises the RuntimeError exception, which is
the ML equivalent of calling runerror.
True definitions are as in the C code, except again, fields are not named.
307a. hdefinition of def for µScheme 307ai≡ (S380a)
datatype def = VAL of name * exp
| EXP of exp
| DEFINE of name * lambda
§5.2
Unit tests and other extended definitions are relegated to Appendix O. Abstract syntax
The rest of this section defines utility functions on values. and values
Instead of printf, ML provides functions that can create, manipulate, and com‐
bine strings. So instead using something like Chapter 1’s extensible print func‐
tion, this chapter builds strings using string‐conversion functions. One example,
valueString, which converts an ML value to a string, is shown here. The other
string‐conversion functions are relegated to the Supplement.
Function valueString is primarily concerned with S‐expressions. An atom is
easily converted, but a list made up of cons cells (PAIRs) requires care; the cdr is
converted by a recursive function, tail, which implements the same list‐printing
algorithm as the C code. (The algorithm, which goes back to McCarthy, is imple‐
mented by C function printtail on page S332.) Function tail is defined inside
valueString, with which it is mutually recursive.
307b. hdefinition of valueString for µScheme, Typed µScheme, and nanoML 307bi≡ (S380a)
Embedding and projection for Booleans is a little different; unlike some pro‐
jection functions, projectBool is total: it always succeeds. Function projectBool
reflects the operational semantics of µScheme, which treats any value other than
#f as a true value.2
308b. hutility functions on values (µScheme, Typed µScheme, nanoML) 308ai+≡ (S379) ◁ 308a 308c ▷
fun embedBool b = BOOLV b embedBool : bool ‑> value
fun projectBool (BOOLV false) = false projectBool : value ‑> bool
| projectBool _ = true
The same Boolean projection function is used in Chapter 2, but without the jargon;
there, the projection function is called istrue.
A list of values can be embedded as a single value by converting ML’s :: and []
to µScheme’s PAIR and NIL. The corresponding projection is left as Exercise 4.
308c. hutility functions on values (µScheme, Typed µScheme, nanoML) 308ai+≡ (S379) ◁ 308b
A VAR or SET form looks up a name x in rho. The name is expected to be bound
to a mutable reference cell, which is ML’s version of a pointer to a location allocated
on the heap. Such locations are read and written not by using special syntax like
C’s *, but by using functions ! and :=, which are in the initial basis of Standard ML.
(The := symbol, like the + symbol, is an ordinary ML function that is declared to be
infix.)
309b. hmore alternatives for ev for µScheme 309bi≡ (309a) 309c ▷
| ev (VAR x) = !(find (x, rho))
| ev (SET (x, e)) =
let val v = ev e
in find (x, rho) := v;
v
end
Because the right‐hand side of SET, here called e, is evaluated in the same environ‐ BEGIN 306
ment as the SET, it can be evaluated using ev. BOOLV 306
type env 304
An IF or WHILE form must interpret a µScheme value as a Boolean. Both forms type exp 306
use the projection function projectBool. find 305b
309c. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 309b 309d ▷ IFX 306
LITERAL 306
| ev (IFX (e1, e2, e3)) = ev (if projectBool (ev e1) then e2 else e3)
NIL 306
| ev (WHILEX (guard, body)) =
NUM 306
if projectBool (ev guard) then PAIR 306
(ev body; ev (WHILEX (guard, body))) RuntimeError
else S213b
BOOLV false SET 306
type value 306
The code used to evaluate a while loop is nearly identical to the rule for lowering valueString 307b
while loops in Chapter 3 (page 214). VAR 306
A BEGIN form is evaluated by evaluating its subexpressions in order, retaining WHILEX 306
the value of the last one. The subexpressions are evaluated by auxiliary function b,
which remembers the value of the last expression in an accumulating parameter
lastval. To ensure that an empty BEGIN is evaluated correctly, lastval is initially
a µScheme #f.
309d. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 309c 310a ▷
| ev (BEGIN es) =
let fun b (e::es, lastval) = b (es, ev e)
| b ( [], lastval) = lastval
in b (es, BOOLV false)
end
A LAMBDA form captures a closure, which is as simple as in C.
310a. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 309d 310b ▷
| ev (LAMBDA (xs, e)) = CLOSURE ((xs, e), rho)
The pattern e as APPLY (f, args) matches an APPLY node. On the right‐hand side,
e stands for the entire node, and f and args stand for the children.
A closure is applied by first creating fresh locations to hold the values of the
actual parameters. In Chapter 2, the locations are allocated by function allocate;
here, they are allocated by the built‐in function ref. Calling ref v allocates a new
location and initializes it to v . The ML expression map ref actuals does half the
work of Chapter 2’s bindalloclist; the other half is done by bindList.
310c. happly closure clo to args 310ci≡ (310b)
let val ((formals, body), savedrho) = clo
val actuals = map ev args
in eval (body, bindList (formals, map ref actuals, savedrho))
handle BindListLength =>
raise RuntimeError ("Wrong number of arguments to closure; " ^
"expected (" ^ spaceSep formals ^ ")")
end
If the number of actual parameters doesn’t match the number of formal param‐
eters, bindList raises the BindListLength exception, which eval catches using
handle. The handler then raises RuntimeError.
A LET form is most easily evaluated by first unzipping the list of pairs bs into a
pair of lists (names, rightSides); function ListPair.unzip is from the ListPair
module in ML’s Standard Basis Library. Each right‐hand side is then evaluated
with ev and stored in a fresh location by ref. To do the whole list at once, I use
map with the function composition rev o ev. Finally, the body of the LET is evalu‐
ated in a new environment built by bindList; since ev works only with the current
rho, the body must be evaluated by eval.
310d. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 310b 310e ▷
ListPair.unzip : ('a * 'b) list ‑> 'a list * 'b list
| ev (LETX (LET, bs, body)) =
let val (names, rightSides) = ListPair.unzip bs
in eval (body, bindList (names, map (ref o ev) rightSides, rho))
end
A LETSTAR form, by contrast, is more easily evaluated by walking the bindings one
pair at a time.
310e. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 310d 311a ▷
| ev (LETX (LETSTAR, bs, body)) =
let fun step ((x, e), rho) = bind (x, ref (eval (e, rho)), rho)
in eval (body, foldl step rho bs)
end
As in Chapter 2, a LETREC form is evaluated by first building a new environment
rho' that binds each name to a fresh location, then evaluating each right‐hand side
in the new environment, updating the fresh locations, and finally evaluating the
body. The updates are performed by List.app, which, just like µScheme’s app, ap‐
plies a function to every element of a list, just for its side effect. Functions List.app
and map are used here with anonymous functions, each of which is written with fn—
which is ML’s way of writing lambda.
311a. hmore alternatives for ev for µScheme 309bi+≡ (309a) ◁ 310e §5.3
List.app : ('a ‑> unit) ‑> 'a list ‑> unit Evaluation
| ev (LETX (LETREC, bs, body)) =
let val (names, rightSides) = ListPair.unzip bs
311
val rho' =
bindList (names, map (fn _ => ref (unspecified())) rightSides, rho)
val updates = map (fn (x, rightSide) => (x, eval (rightSide, rho'))) bs
in List.app (fn (x, v) => find (x, rho') := v) updates;
eval (body, rho')
end
5 in
end
evaldef (VAL (f, LAMBDA lambda), rho)
The EXP form doesn’t bind a name; evaldef just evaluates the expression, binds
Interlude: the result to it, and responds with the value.
µScheme in ML 312b. hdefinitions of eval and evaldef for µScheme 309ai+≡ ◁ 312a
| evaldef (EXP e, rho) =
312 let val v = eval (e, rho)
val rho = withNameBound ("it", rho)
val _ = find ("it", rho) := v
in (rho, valueString v)
end
The differences between VAL and EXP are subtle: for VAL, the semantics demands
that the name be added to environment rho before evaluating expression e. For EXP,
the name it isn’t bound until after evaluating the first EXP form.
µScheme primitives like + and cons have ML counterparts like + and PAIR. But the
ML counterparts operate on ML values, and the µScheme versions must operate
on µScheme values. Each µScheme primitive is implemented just as in Chapter 2
(chunk 161b): take µScheme values as arguments, project them to ML values, apply
an ML primitive, and embed the ML result into µScheme. But the code is structured
differently: instead of projecting arguments and embedding a result, I embed the
function. Each primitive function is written with its own most natural ML type,
then is embedded into the type of µScheme primitives: exp * value list ‑> value.
The embedding takes care of the function’s arguments and results. (Because no
µScheme function ever acts as an ML function, a corresponding projection is not
needed.)
The embedding for a function depends on the function’s type. Each embedding
is composed of other functions, which gradually “lift” a primitive function from
its native ML type to type exp * value list ‑> value. For example, the primitive
function + passes through these types:
• The lifted function is embedded into a function that can be applied to a list
of values; after this second lifing, its type is now value list ‑> value.
• Finally, the twice‐lifted function is embedded into a function that is given not
just a list of values but also the syntax of the expression in which the µScheme
primitive appears; its final type is exp * value list ‑> value, which is the
type of the primitive used in the evaluator.
Let’s look at these lifting steps in reverse order, from last to first.
The final lifting step is given a function f of type value list ‑> value, and
it produces a new function inExp f of type exp * value list ‑> value. The new
function applies f, and if applying f raises the RuntimeError exception, it handles
the exception, adds to the error message, and re‐raises the exception.
313a. hutility functions for building primitives in µScheme 313ai≡ (S382a) 313b ▷
fun inExp f = inExp : (value list ‑> value) ‑> (exp * value list ‑> value) §5.4
fn (e, vs) => f vs Defining and
handle RuntimeError msg => embedding
raise RuntimeError ("in " ^ expString e ^ ", " ^ msg) primitives
The middle lifting step is given a function f that takes one or two arguments
313
of type value and returns a result of type value, and it produces a new func‐
tion unaryOp f or binaryOp f of type value list ‑> value. The new function uses
pattern matching to extract the expected number of arguments and pass them
to f. If the number of arguments is unexpected, function arityError raises
RuntimeError.
313b. hutility functions for building primitives in µScheme 313ai+≡ (S382a) ◁ 313a 313c ▷
unaryOp : (value ‑> value) ‑> (value list ‑> value)
binaryOp : (value * value ‑> value) ‑> (value list ‑> value)
Functions unaryOp and binaryOp help implement any µScheme primitive that is a
“unary operator” or “binary operator.”
The first lifting step is given a function like + that expects and returns ML
integers, and it produces a new function arithOp + of type value list ‑> value.
The anonymous function passed to binaryOp has type value * value ‑> value.
313c. hutility functions for building primitives in µScheme 313ai+≡ (S382a) ◁ 313b 314a ▷ DEFINE 307a
arithOp: (int * int ‑> int) ‑> (value list ‑> value) eval 309a
fun arithOp f = binaryOp (fn (NUM n1, NUM n2) => NUM (f (n1, n2))
evaldef 311c
EXP 307a
| (NUM n, v) => hreport v is not an integer 314bi type exp 306
| (v, _) => hreport v is not an integer 314bi expString S383b
) find 305b
intString S214c
Now µScheme primitives like + and * can be defined by applying first arithOp and
LAMBDA 306
then inExp to their ML counterparts. NUM 306
The µScheme primitives are organized into a list of (name, function) pairs, in RuntimeError
Noweb code chunk hprimitives for µScheme :: 313di. Each primitive on the list has S213b
VAL 307a
type value list ‑> value. In chunk S382a, each primitive is passed to inExp, and
type value 306
the results are used build µScheme’s initial environment.3 The list of primitives valueString 307b
begins with these four elements: withNameBound
311b
313d. hprimitives for µScheme :: 313di≡ (S382a) 314c ▷
("+", arithOp op + ) ::
("‑", arithOp op ‑ ) ::
("*", arithOp op * ) ::
("/", arithOp op div) ::
The remaining type predicates, the list primitives, and the printing primitives are
defined in Appendix O.
The ML interpreter presented in Sections 5.3 and 5.4 uses the same overall design
as the C interpreters of Chapters 1 and 2. But the many small differences in the two
languages add up to a different programming experience; the ML version is more
compact and more reliable. The two experiences compare as follows:
• Both interpreters allocate mutable locations on the heap, which they operate
on with C pointer syntax (*) or ML primitive functions (! and :=). The C code
leaks memory like crazy; plugging all the leaks would require a garbage col‐
lector considerably more elaborate than the one in Chapter 4. ML ships with
a comprehensive garbage collector built in.
• Both interpreters use the same abstraction to represent abstract syntax trees:
a tagged sum of products. Thanks to the little data‐description language of
Chapter 1, the representations are even specified similarly. But C’s unions
are unsafe: making the alt tag consistent with the payload is up to the pro‐
grammer. ML’s algebraic data types guarantee consistency. The C code does
offer one advantage, however: in the source code, the definitions of struct
Value and struct Exp can appear separately. In the ML code, the definitions
value and exp, because they are mutually recursive, must appear adjacent in
the source code so they can be connected with and.
• Both interpreters manage run‐time errors in the same way. An error may be
detected and signaled anywhere; C code uses runerror, which calls longjmp
(in C), and ML code uses raise. And in both interpreters, an error once de‐
tected is handled in a central place, using setjmp or handle, as described in
the Supplement.
• Both interpreters use functions, like “length of a list” and “find a name in an
environment,” that could in principle be polymorphic. But only ML code can
define a function that is actually polymorphic. The C code in Chapters 1 and 2
must define a new length function for every type of list and a new find func‐
tion for every type of environment.
• C code can use printf, and it can even define new functions that resemble §5.6
printf, like print. ML code has nothing comparable: because the ML type Free and bound
checker won’t check the types of the arguments based on a format string, variables: Deeper
ML code can only print strings—so it must use string‐conversion functions. into µScheme
315
• To define primitives, both interpreters use first‐order embedding and pro‐
jection functions to embed and project numbers (projectint32 and mkNum
or projectInt and embedInt) and Booleans. But only the ML code can em‐
bed functions, using binaryOp, intcompare, and so on. And in ML, opera‐
tions like / and div are functions, not syntax, so they can be embedded into
µScheme directly. Their counterparts in C require significant “glue code.”
the name + is a free variable, but n is a bound variable. Every variable that appears
in an expression is either free or bound.
Each variable that appears in a definition is also free or bound. For example, in binaryOp 313b
BOOLV 306
(define map (f xs) embedBool 308b
(if (null? xs) equalatoms S380c
NIL 306
'()
NUM 306
(cons (f (car xs)) (map f (cdr xs))))) RuntimeError
S213b
the names null?, cons, car, and cdr are free, and the names map, f, and xs are unaryOp 313b
bound. (And if is not a name; it is a reserved word that, like if in ML or C, marks a type value 306
valueString 307b
syntactic form.)
Free variables enable compilers to represent closures efficiently. According to
the operational semantics, evaluating a lambda expression captures the entire en‐
vironment ρc :
x1 , . . . , xn all distinct .
hLAMBDA(hx1 , . . . , xn i, e), ρc , σi ⇓ h(|LAMBDA(hx1 , . . . , xn i, e), ρc |), σi
(MĸCLOſURE)
Does the closure really need all the information in ρc ? How is ρc used?
x ∈ fv(VAR(x))
y ∈ fv(e)
.
x ∈ fv(ſET(x, e)) y ∈ fv(ſET(x, e))
A variable is also free in a WHıLE expression if and only if it is free in one of the
subexpressions:
y ∈ fv(e1 ) y ∈ fv(e2 )
.
y ∈ fv(WHıLE(e1 , e2 )) y ∈ fv(WHıLE(e1 , e2 ))
y ∈ fv(ei )
.
y ∈ fv(BEGıN(e1 , . . . , en ))
y ∈ fv(e) y ∈ fv(ei )
.
y ∈ fv(APPLY(e, e1 , . . . , en )) y ∈ fv(APPLY(e, e1 , . . . , en ))
Finally, an interesting case! A variable is free in a LAMBDA expression if it is free in
the body and it is not one of the arguments:
y ∈ fv(e) y∈ / {x1 , . . . , xn }
.
y ∈ fv(LAMBDA(hx1 , . . . , xn i, e))
The various LET forms require care. A variable is free in an ordinary LET if it is
free in the right‐hand side of any binding, or if it is both free in the body and not
bound by the LET. §5.7
Summary
y ∈ fv(ei ) y ∈ fv(e) y∈ / {x1 , . . . , xn }
y ∈ fv(LET(hx1 , e1 , . . . , xn , en i, e)) y ∈ fv(LET(hx1 , e1 , . . . , xn , en i, e)) 317
The similarity between the second LET rule and the LAMBDA rule shows a kinship
between LET and LAMBDA.
The rules for LETREC are almost identical to the rules for LET, except that in a
LETREC, the bound names xi are never free:
y ∈ fv(ei ) y∈ / {x1 , . . . , xn }
,
y ∈ fv(LETREC(hx1 , e1 , . . . , xn , en i, e))
y ∈ fv(e) y∈ / {x1 , . . . , xn }
.
y ∈ fv(LETREC(hx1 , e1 , . . . , xn , en i, e))
As usual, a LETſTAR rule would be a nuisance to write directly. Instead, I treat
a LETſTAR expression as a set of nested LET expressions, each containing just one
binding. And an empty LETſTAR behaves just like its body.
y ∈ fv(e)
y ∈ fv(LETſTAR(hi, e))
5.7 SUMMARY
FREE VARıABLE A variable that is defined outside the function in which it appears.
The meaning of a free variable depends on context. The idea of free variable
generalizes beyond function definitions to include any language construct
that introduces new variables, like a let expression. Variables that aren’t
free are BOUND.
MUTUAL RECURſıON (DATA) Two or more ALGEBRAıC DATA TYPEſ, each of which 319
can contain a value of another. Defined using datatype and keyword and;
in ML, the and always signifies mutual recursion. In most languages in this
book, types exp and value are mutually recursive: an exp can contain a literal
value, and a value might be a closure, which contains an exp.
TYPE VARıABLE In ML, a name that begins with a quote mark, like 'a. Stands for
an unknown type. When used in an ML type, a type variable makes the type
POLYMORPHıC.
To learn Standard ML, you have several good choices. The most comprehensive
published book is by Paulson (1996), but it may be more than you need. The much
shorter book by Felleisen and Friedman (1997) introduces ML using an idiosyn‐
cratic, dialectical style. If you can learn from that style, the information is good.
If you are a proficient C programmer, you might like the book by Ullman (1997).
This book has helped many C programmers make a transition to ML, but it also has
a problem: the ML that it teaches is far from idiomatic.
There are also several good unpublished resources. Harper’s (1986) introduc‐
tion is short, sweet, and easy to follow, but it is for an older version of Standard ML.
More recently, Harper (2011) has released an unfinished textbook on programming
in Standard ML; it is up to date with the language, but the style is less congenial to
beginners. Tofte (2009) presents “tips” on Standard ML, which I characterize as a
20‐page quick‐reference card. You probably can’t get by on the “tips” alone, but
when you are working at the computer, they are useful.
5.8 EXERCıſEſ
The exercises are summarized in Table 5.3 on page 321. The highlights encourage
you to extend or improve µScheme:
• In Exercise 10, you use facts about free variables to change the representation
of closures, and you measure to see if the change matters.
1. Syntactic sugar for cond. Section 2.13.2 (page 163) describes syntactic sugar
for Lisp’s original conditional expression: the cond form. Add a cond form to
5 µScheme. Start with this code:
322. hrows added to ML µScheme’s exptable in exercises [[prototype]] 322i≡
µScheme in ML val qa = bracket ("[question answer]", pair <$> exp <*> exp)
in desugarCond <$> many qa
322 end
)
Now recompile; type‐error messages will tell you what other code you
have to change.
For the parser, you may find the following function useful:
and it is designed for you to adapt old syntax to new syntax; just drop it
into the parser wherever LAMBDA is used.
(b) As a complement to the varargs lambda, write a new apply primitive
such that
(apply f '(1 2 3))
is equivalent to
(f 1 2 3)
Sadly, you can’t use PRIMITIVE for this; you’ll have to invent a new kind
of thing that has access to the internal eval.
(c) Demonstrate these utilities by writing a higher‐order µScheme func‐
tion cons‑logger that counts cons calls in a private variable. It should
operate as follows:
(d) Rewrite the APPLYCLOſURE rule (page 316) to account for the new ab‐
stract syntax and behavior.
(a) Define function projectList of type value ‑> value list. If projec‐
tion fails, projectList should raise the RuntimeError exception.
(b) Rewrite function embedList to use foldr.
5. Reusable embedding for integer functions. In Section 5.4, functions arithOp and
comparison use the same code fragment for embedding functions that con‐
sume integers. Break this embedding out into its own function, intBinary
<$> S249a
of type (int * int ‑> 'a) ‑> (value * value ‑> 'a). Rewrite arithOp and <*> S247b
comparison to use intBinary. bracket S264a
type exp 306
6. Embedding by composing firstorder functions. In Section 5.4 (page 312), the exp S387b
primitive functions are defined by applying higher‐order embedding func‐ LeftAsExercise
S213b
tions to ML primitives. In this exercise, you instead compose the ML primi‐
many S253a
tives on the right with first‐order projection functions and on the left with pair S249b
first‐order embedding functions. Embedding functions should be total;
that is, application of an embedding function should always succeed. But
projection functions can be partial; application of a projection function can
fail, in which case it should raise RuntimeError.
(a) Define a projection function of type value list ‑> value * value and
another of type value list ‑> value.
(b) Define a projection function of type value * value ‑> int * int
(c) Find an embedding function of type int ‑> value, or if you can’t find
one, define one.
(d) Find an embedding function of type bool ‑> value, or if you can’t find
one, define one.
(e) Using your embedding and projection functions, redefine the µScheme
You may wish to revisit the material on proofs and derivations in Section 1.7
(page 55).
9. Proof: A closure needs only the free variables of its code part. In this exercise,
you prove that the evaluation of an expression doesn’t depend on arbitrary
bindings in the environment, but only on the bindings of the expression’s
free variables.
If X is a set of variables, we can ask what happens to an environment ρ if we
remove the bindings of all the names that are not in the set X . The modified
environment is written ρ X , and it is called the restriction of ρ to X . You will
prove, by structural induction on derivations, that if he, ρ, σi ⇓ hv, σi, then
he, ρ fv(e) , σi ⇓ hv, σi. (This theorem also justifies the syntactic sugar for
short‐circuit || described in Section 2.13.3 on page 164. And it is related to a
similar theorem from Exercise 52 on page 195 in Chapter 2.)
To structure the proof, I recommend you introduce a definition and a lemma.
10. Smaller closures and their performance. The result proved in Exercise 9 can be
used to optimize code. In chunk 310a, a LAMBDA expression is evaluated by §5.8
capturing a full environment ρ. Exercises
(a) Modify the code to capture a restricted environment that contains only 325
the free variables of the LAMBDA expression. That is, instead of allo‐
cating the closure (|LAMBDA(hx1 , . . . , xn i, e), ρ |), allocate the smaller
closure (|LAMBDA(hx1 , . . . , xn i, e), ρ X |), where the set X is defined
by X = fv(LAMBDA(hx1 , . . . , xn i, e)).
(b) Measure the modified interpreter to see if the optimization makes a dif‐
ference. As a sufficiently long‐running computation, you could try a
quadratic sort of a very long list, or an exponential count of the num‐
ber of distinct subsequences of an integer sequence that sum to zero.
Or you could run a computation using the metacircular evaluator in
Section E.1.
CHAPTER 6 CONTENTſ
6.1 TYPED IMPCORE: 6.6.5 Typing rules
A ſTATıCALLY TYPED for Typed µScheme 361
ıMPERATıVE CORE 329 6.6.6 Type equivalence and
6.1.1 Concrete syntax type‐variable renaming 367
of Typed Impcore 330 6.6.7 Instantiation and
6.1.2 Predefined functions renaming by
of Typed Impcore 331 capture‐avoiding
6.1.3 Types, values, and substitution 371
abstract syntax 6.6.8 Subverting the type
of Typed Impcore 331 system through
6.1.4 Type system variable capture 376
for Typed Impcore 333 6.6.9 Preventing capture with
6.1.5 Typing rules type‑lambda 377
for Typed Impcore 334 6.6.10 Other building blocks of a
6.2 A TYPE‐CHECĸıNG type checker 378
ıNTERPRETER 6.7 TYPE ſYſTEMſ Aſ
FOR TYPED IMPCORE 337 THEY REALLY ARE 383
6.2.1 Type checking 338
6.8 SUMMARY 383
6.2.2 Typechecking definitions 341
6.8.1 Key words and phrases 384
6.3 EXTENDıNG TYPED IMPCORE 6.8.2 Further reading 385
WıTH ARRAYſ 343
6.3.1 Types for arrays 343
6.9 EXERCıſEſ 386
6.3.2 New syntax for arrays 343 6.9.1 Retrieval practice and
6.3.3 Rules for type other short questions 387
constructors: Formation, 6.9.2 Type‐system
introduction, and fundamentals 388
elimination 345 6.9.3 Extending a
monomorphic language 389
6.4 COMMON TYPE
6.9.4 Coding in a polymorphic
CONſTRUCTORſ 348
language 389
6.5 TYPE ſOUNDNEſſ 350 6.9.5 Extending a polymorphic
6.6 POLYMORPHıC TYPE ſYſTEMſ language 390
AND TYPED µSCHEME 351 6.9.6 Typing derivations 394
6.6.1 Concrete syntax… 352 6.9.7 Implementing type
6.6.2 A replacement for checking 394
type‐formation rules: Kinds 352 6.9.8 Metatheory 396
6.6.3 The heart of polymorphism: 6.9.9 Metatheory about
Quantified types 356 implementation 397
6.6.4 Abstract syntax, values, 6.9.10 Capture‐avoiding
and evaluation substitution 397
of Typed µScheme 361 6.9.11 Loopholes in type systems 398
Type systems for Impcore and µScheme 6
But in a typed language a separate sort function must be
defined for each type, while in a typeless language syntactic
checking is lost. We suggest that a solution to this problem
is to permit types themselves to be passed as a special
kind of parameter, whose usage is restricted in a way
which permits the syntactic checking of type correctness.
John Reynolds, Towards a Theory of Type Structure
The languages of the preceding chapters, Impcore and µScheme, are dynamically
typed, which is to say that many faults, such as applying a function to the wrong
number of arguments, adding non‐numbers, or applying car to a symbol, are not
detected until run time. Dynamically typed languages are very flexible, but on any
given execution, a fault might surprise you; even a simple mistake like typing cdr
when you meant car might not have been detected on previous runs. And using
cdr instead of car doesn’t cause a fault right away: cdr simply returns a list in a
context where you were expecting an element. But if, for example, you then try to
add 1 to the result of applying cdr, that is a checked runtime error: adding 1 to a list
instead of a number. To rule out such errors at compile time, without having to run
the faulty code, a programming language can use static typing.
Static typing is implemented by a compile‐time analysis, which decides if the
analyzed code is OK to run. All such analyses build on the same two approaches:
type checking and type inference. In type checking, every variable and formal param‐
eter is annotated with a type, which restricts the values that the variable or parame‐
ter may have at run time. Type checking is used in such languages as Ada, Algol, C,
C++, Go, Java, Modula‐3, Pascal, and Rust. In type inference, also called type recon‐
struction, variables and parameters need not be annotated; instead, each variable
or parameter is given a type by an algorithm, which looks at how the variable or
parameter is used—the types are reconstructed from the code. Type inference is
used in such languages as Haskell, Hope, Miranda, OCaml, Standard ML, and Type‐
Script. In both approaches, the decision about whether it is OK to run a program is
made according to the rules of a language‐dependent type system.
Effective types do much more than just rule out programs that might commit
run‐time errors; types also act as documentation—the more expressive the type
327
system, the better the documentation. In a truly expressive system, the name and
type of a function often suffice to show what the function is supposed to do.
Type systems, type checking, and type inference can be used in many ways.
One of the most effective uses is to help guarantee safety. In a safe language, mean‐
6 ingless operations (adding non‐numbers, dereferencing a null pointer, and so on)
are either ruled out or are detected and reported. Although safety can be guaran‐
teed by checking every operation at run time—as in Impcore, µScheme, and full
Scheme—a good static type system performs most checks at compile time. A sim‐
Type systems for
ple static type system can guarantee properties like these:
Impcore and µScheme
• Numbers are added only to numbers.
328
• Every function receives the correct number of arguments.
• Only Booleans are used in if expressions.
• Primitives car and cdr are applied only to lists.
When these properties are guaranteed, potentially meaningless operations are re‐
ported to the programmer right away, before a program is shipped to its users. And
the implementations of addition, function call, and if don’t have to check their
operands at run time.
To guarantee safety, a type system must be crafted such that if it accepts a pro‐
gram, its rules guarantee that no meaningless operations can be executed. The
guarantee is established by a typesoundness theorem, an idea so popular that it has
its own slogan: “well‐typed programs don’t go wrong.” The slogan is lovely, but not
a cure‐all; “going wrong” has a precise, technical meaning, which is usually nar‐
row. For example, “going wrong” does not typically include unwanted behaviors
like these:
• A number is divided by zero.
• The car or cdr primitive is applied to an empty list.
• A reference to an element of an array falls outside the bounds of that array.
These misbehaviors can be ruled out by a static type analysis, but the types get com‐
plicated, so they often aren’t. Most general‐purpose languages keep their types
simple, and they guarantee safety by supplementing the static type system with
run‐time checks for errors like division by zero or array access out of bounds.
A safe language shouldn’t put the programmer in a straitjacket. When a type
system is overly restrictive, programmers complain, often using slang terms like
“strong typing.” And restrictive type systems can make it hard to reuse code. For ex‐
ample, Pascal’s type system notoriously made it impossible to write a function ca‐
pable of sorting arrays of different lengths. As another example, in Chapters 1 and 2,
C’s type system requires a distinct set of list functions for each possible list type,
even though the functions defined on different types have identical bodies. Such
duplication can be avoided, while maintaining safety and type checking, by using a
polymorphic type system, like the one described in Section 6.6 of this chapter. Poly‐
morphic type systems provide abstractions that can be parameterized by types; one
example of such an abstraction is the C++ template.
Or restrictions can be dodged by using unsafe language features, like casts to
and from void * in C. Unsafe features are useful in situations like these:
• You want to write a program that manipulates memory directly, like a device
driver or a garbage collector. Such a program would ideally be safe, but how
best to make such a thing safe—say, by combining a sophisticated type system
with a formal proof of correctness—is a topic of ongoing research.
• You want a relatively simple type system and you don’t want to pay overhead
for run‐time checks. For example, you like the simplicity of C, and you love
that casting a pointer from one type to another costs nothing. And although
casts are unsafe in general, you’ve convinced yourself that yours are safe.
Even in a language with unsafe features, static typing is useful: types document the
code, and they can prevent many run‐time errors, even if a few slip through. The
ones that slip through are called unchecked run‐time errors—it’s those errors that
make a language unsafe. The best designs allow unsafe features only in limited §6.1
contexts; to see this done well, study Modula‐3 (Nelson 1991). Typed Impcore:
Type systems are highly developed, and in the next four chapters you will learn A statically typed
how they work and how to use them effectively. In this chapter, you will learn about imperative core
type systems and type checking. You will study not one language but two: Typed 329
Impcore and Typed µScheme. Typed Impcore is straightforward; it models the re‐
strictive type systems of such languages as Pascal and C. Typed Impcore introduces
type systems and serves as an uncomfortably restrictive example. Typed µScheme
is more ambitious; although its design starts from µScheme, it ends up requiring
so many type annotations it feels very different. Typed µScheme introduces poly‐
morphism and serves as an eye‐opening example.
Typed µScheme is powerful, but its explicit annotations make it unpleasant
to write. This unpleasantness is relieved by the more advanced type systems de‐
scribed in Chapters 7 to 9. Chapter 7 describes nano‐ML. Nano‐ML eliminates an‐
notations, and yet it provides most of the polymorphism that Typed µScheme en‐
joys. It does so by using the HindleyMilner type system, which defines a form of poly‐
morphism that can be inferred. The Hindley‐Milner type system forms the core
of many of today’s innovative, statically typed languages, including Standard ML,
Haskell, OCaml, Agda, Idris, and many others. It works most effectively when cou‐
pled with user‐defined algebraic data types, which are the main idea of Chapter 8.
Chapter 9 presents a different alternative to Typed µScheme: Molecule. Like
Typed µScheme, Molecule uses annotations, but the annotations that direct poly‐
morphism are applied to entire modules, not to individual functions. The resulting
language provides the explicit control you get with Typed µScheme, without the
notational burden.
A type system finds errors that might otherwise be hard to detect. (A type system
may also help determine how values are represented and where variables can be
stored, but such topics are beyond the scope of this book.) A type system worth
studying should therefore be coupled with a language that provides ample opportu‐
nities to commit detectable errors—and that is simple enough to learn easily. Imp‐
core is suitably simple, but the only readily detectable error is to pass the wrong
number of arguments to a function. To create a few more opportunities for de‐
tectable errors, I have designed Typed Impcore:
• A value may be an integer, a Boolean, an array of values, or unit (page 334).
Now you can try to use the wrong species of value.
• Every variable and expression must have a type that is known at compile time.
The types of some variables, such as the formal parameters of functions, are
written down explicitly, much as they are in C and Java. Precise, formal rules
determine whether an expression has a type and what the type is.
• The typing rules are implemented by a type checker. The type checker per‐
mits a definition to be evaluated only if all its expressions have types. The
type checker I present in this chapter checks only integer and Boolean types;
extending it to check arrays is left to you (Exercise 18).
def ::= exp
| (use filename)
| (val variablename exp)
| (define type functionname (formals) exp)
6 | unittest
unittest ::= (check‑expect exp exp)
| (check‑assert exp)
Type systems for | (check‑error exp)
Impcore and µScheme | (check‑type‑error def )
330 | (check‑function‑type function ( type ‑> type))
exp ::= literal
| variablename
| (set variablename exp)
| (if exp exp exp)
| (while exp
exp)
| (begin exp )
| (functionname exp )
formals ::= [variablename : type]
• Impcore’s type system is sound (Exercise 26), which means informally that in
every execution, at every point in the program, when an expression produces
a value, that value is consistent with the expression’s type. Type soundness
ensures that if a program passes the type checker, it does not suffer from type
errors at run time.
Typed Impcore is presented in two stages: first the integer and Boolean parts, then
(Section 6.3) arrays.
Typed Impcore has the concrete syntax shown in Figure 6.1. Unlike untyped Imp‐
core, Typed Impcore requires that the argument and result types of functions be
declared explicitly. Types are written using new syntax; in addition to Impcore’s
definitions (def ) and expressions (exp), Typed Impcore includes a syntactic cate‐
gory of types (type).
Explicit types are needed only in function definitions:
330. htranscript 330i≡ 331a ▷
‑> (define int add1 ([n : int]) (+ n 1))
add1 : (int ‑> int)
‑> (add1 4)
5 : int
‑> (define int double ([n : int]) (+ n n))
double : (int ‑> int)
‑> (double 4)
8 : int
Types provide good documentation, and to document a function’s type in a
testable way, Typed Impcore adds a new unit‐test form, check‑function‑type:
331a. htranscript 330i+≡ ◁ 330 331b ▷
‑> (check‑function‑type add1 (int ‑> int))
‑> (check‑function‑type double (int ‑> int))
Because unit tests are not run until the end of a file, a function’s type test can— §6.1
and should—be placed before its definition. Typed Impcore:
331b. htranscript 330i+≡ ◁ 331a 331c ▷ A statically typed
‑> (check‑function‑type positive? (int ‑> bool)) imperative core
‑> (define bool positive? ([n : int]) (> n 0))
331
Unlike C, Typed Impcore accepts only Boolean conditions:
331c. htranscript 330i+≡ ◁ 331b 344a ▷
‑> (if 1 77 88)
type error: Condition in if expression has type int, which should be bool
‑> (if (positive? 1) 77 99)
77 : int
The predefined functions of Typed Impcore do the same work at run time as their
counterparts in Chapter 1, but because they include explicit types for arguments
and results, their definitions look different. And because Typed Impcore has no
Boolean literals, falsehood is written as (= 1 0), and truth as (= 0 0).
331d. hpredefined Typed Impcore functions 331di≡ 331e ▷
(define bool and ([b : bool] [c : bool]) (if b c b))
(define bool or ([b : bool] [c : bool]) (if b b c))
(define bool not ([b : bool]) (if b (= 1 0) (= 0 0)))
A funty describes just one type; for example, FUNTY ([INTTY, INTTY], BOOLTY)
describes the type of a function that takes two integer arguments and returns a
Boolean result. In Typed Impcore, a function or variable has at most one type,
which makes Typed Impcore monomorphic.
Types are printed with the help of functions typeString and funtyString,
which are defined in Appendix P.
Types are checked for equality by mutually recursive functions eqType and
eqTypes:
332a. htypes for Typed Impcore 331fi+≡ (S391a) ◁ 331f 332b ▷
eqType : ty * ty ‑> bool
6 eqTypes : ty list * ty list ‑> bool
Types should always be checked for equality using eqType, not the built‐in = oper‐
ator. As shown in Section 6.6.6 below, a single type can sometimes have multiple
representations, which = reports as different but should actually be considered the
same. Using eqType gets these cases right; if you use =, you risk introducing bugs
that will be hard to find.
Function types are checked for equality using function eqFunty.
332b. htypes for Typed Impcore 331fi+≡ (S391a) ◁ 332a
eqFunty : funty * funty ‑> bool
fun eqFunty (FUNTY (args, result), FUNTY (args', result')) =
eqTypes (args, args') andalso eqType (result, result')
Moving to run time, there are only two forms of value. Values of integer,
Boolean, or unit type are represented using the NUM form; values of array types
are represented using the ARRAY form.
332c. hdefinitions of exp and value for Typed Impcore 332ci≡ (S391b) 332d ▷
datatype value = NUM of int
| ARRAY of value array
And finally the syntax. Typed Impcore includes every form of expression found
in untyped Impcore, plus new forms for equality, printing, and array operations.
332d. hdefinitions of exp and value for Typed Impcore 332ci+≡ (S391b) ◁ 332c
datatype exp = LITERAL of value
| VAR of name
| SET of name * exp
| IFX of exp * exp * exp
| WHILEX of exp * exp
| BEGIN of exp list
| APPLY of name * exp list
hTyped Impcore syntax for equality and printing 332ei
harray extensions to Typed Impcore’s abstract syntax 344di
In Typed Impcore, the =, print, and println operations cannot be imple‐
mented by primitive functions, because they operate on values of more than one
type: they are polymorphic. In a monomorphic language like Typed Impcore or C,
each polymorphic primitive needs its own syntax.
332e. hTyped Impcore syntax for equality and printing 332ei≡ (332d)
| EQ of exp * exp
| PRINTLN of exp
| PRINT of exp
A func contains no types; types are needed only during type checking, and the func
representation is used at run time, after all types have been checked.
By design, Typed Impcore’s static type system is more restrictive than Impcore’s
dynamic type system. For example, because the static type system distinguishes
integers from Booleans, it prevents you from using an integer to control an if ex‐
pression. This restriction should not burden you overmuch; if you have an integer i
that you wish to treat as a Boolean, simply write (!= i 0). Similarly, if you have a
Boolean b that you wish to treat as an integer, write (if b 1 0). Typed Impcore also ARRAYTY 331f
prevents you from assigning the result of a while loop to an integer variable—which BOOLTY 331f
FUNTY 331f
there is no good reason to do, because the result is always zero.
type funty 331f
Typed Impcore accepts a definition only if its expressions, also called terms, INTTY 331f
have types. Which terms have types is determined by a formal proof system (the type name 303
type system), which is related to the formal proof system that determines which type ty 331f
UNITTY 331f
terms have values (the operational semantics). To refer to a type, the type sys‐
tem uses a metavariable formed with the Greek letter τ (pronounced “tau,” which
rhymes with “wow”). And to remember the type of a variable or function, the type
system uses a metavariable formed with the Greek letter Γ (pronounced “gamma”).
Typed Impcore’s type system relates these elements: simple types, which are
written τ ; three base types, which are written ıNT, BOOL, and UNıT; one type con
structor, which is written ARRAY; many function types, which are written τf ; and
three type environments, which are written Γξ , Γϕ , and Γρ . The type environments
give the types of global variables, functions, and formal parameters, respectively.
A simple type is a base type or the array constructor applied to a simple type.1
Because each type in Typed Impcore requires its own special‐purpose abstract syn‐
tax, I write the names using the ſMALL CAPſ font, which I conventionally use for
abstract syntax.
6 τ ::= ıNT BOOL UNıT ARRAY(τ )
The type of a function is formed from argument types τ1 to τn and result type τ .
Type systems for
Impcore and µScheme
τf ::= τ1 × · · · × τn → τ
Just as functions are not values, function types are not value types.
334
The integer, Boolean, and array types describe values that represent integers,
Booleans, and arrays. The unit type plays a more subtle role; its purpose is to be dif‐
ferent from the others. It is the type we give to an expression that is executed purely
for its side effect and does not produce an interesting value—like a while loop or
println expression. In most typed languages, an operation that is executed only
for side effects is described by a special type. In C, C++, and Java, this type is called
void, because the type is uninhabited, i.e., there are no values of type void. In the
functional language ML, the special type is called unit, because the unit type has
exactly one inhabitant. For Typed Impcore, the unit type is more appropriate than
void, because in Typed Impcore, every terminating evaluation produces a value.
In ML, the inhabitant of the unit type is the empty tuple, which is written ().
Its value is uninteresting: every expression of type unit produces the same empty
tuple, unless its evaluation fails to terminate or raises an exception. In Typed Imp‐
core, the inhabitant of the unit type is the value 0—but as long as the type system
is designed and implemented correctly, no Typed Impcore program can tell what
the inhabitant is.
A type system is written using the same kind of formal rules we use to write oper‐
ational semantics; only the forms of the judgments are different. Where the judg‐
ments in an operational semantics determine when an expression is evaluated to
produce a value, the judgments in a type system determine when an expression
has a type. The judgments of a type system and the judgments of the correspond‐
ing operational semantics are closely related (Exercise 26).
Typeformation rules
A type system usually begins with rules that say what is and isn’t a type. Typed
Impcore’s types are so simple that rules aren’t really necessary; the tiny grammar
for τ above tells the story. But the judgment form for Typed Impcore is τ is a type ,
and the rules are as follows:
(UNıTTYPE) (INTTYPE) (BOOLTYPE)
,
UNıT is a type ıNT is a type BOOL is a type
τ is a type . (ARRAYTYPE)
ARRAY(τ ) is a type
In the interpreter, every value of type ty (chunk 331f) is a type.
1
In other words, types are defined by induction, and the base types are the base cases. In Typed
Impcore, the type of an array does not include the array’s size.
Typing judgment for expressions
All literals are integers. This rule is sound because there are no Boolean literals in
Typed Impcore: the parser creates only integer literals.
(LıTERAL)
Γξ , Γϕ , Γρ ` LıTERAL(v) : ıNT
x ∈ dom Γρ
(FORMALVAR)
Γξ , Γϕ , Γρ ` VAR(x) : Γρ (x)
x ∈ dom Γρ Γρ (x) = τ
Γξ , Γϕ , Γρ ` e : τ
(FORMALAſſıGN)
Γξ , Γϕ , Γρ ` ſET(x, e) : τ
x∈
/ dom Γρ x ∈ dom Γξ Γξ (x) = τ
Γξ , Γϕ , Γρ ` e : τ
(GLOBALAſſıGN)
Γξ , Γϕ , Γρ ` ſET(x, e) : τ
If the assignment is well typed, its type is the type of the variable and of the value
assigned to it. An assignment could instead be given type unit, but this choice
would rule out such expressions as (set x (set y 0)). In Typed Impcore, as in C,
such expressions are permitted.
A conditional expression is well typed if the condition is Boolean and the two
branches have the same type. In that case, the type of the conditional expression
is the type shared by the branches.
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e 2 : τ Γξ , Γϕ , Γρ ` e3 : τ
(IF)
Γξ , Γϕ , Γρ ` ıF(e1 , e2 , e3 ) : τ
Unlike the rules for literals, variables, and assignment, the IF rule is structured
differently from the ıF rules in the operational semantics. The operational seman‐
tics has two rules: one corresponds to e1 ⇓ BOOLV(#t) and evaluates e2 , and one
corresponds to e1 ⇓ BOOLV(#f) and evaluates e3 . Each rule evaluates just two of
the three subexpressions. The type system, which cares only about the type of e1 ,
not its value, needs only one rule, which checks the types of all three subexpres‐
sions.
A WHıLE loop is well typed if the condition is Boolean and the body is well typed.
The τ that is the type of the body e2 is not used in the conclusion of the rule, because
6 it matters only that type τ exists, not what it is.
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e2 : τ
(WHıLE)
Γξ , Γϕ , Γρ ` WHıLE(e1 , e2 ) : UNıT
Type systems for
Impcore and µScheme As explained above, because a WHıLE loop is executed for its side effect and does
not produce a useful result, it is given the unit type.
336 Like the IF rule, the WHıLE rule is structured differently from the WHıLE
rules in the operational semantics. The operational semantics has two rules: one
corresponds to e1 ⇓ BOOLV(#t) and iterates the loop, and one corresponds to
e1 ⇓ BOOLV(#f) and terminates the loop. The type system needs only one rule,
which checks both types.
A BEGıN expression is well typed if all of its subexpressions are well typed. Be‐
cause every subexpression except the last is executed only for its side effect, it could
legitimately be required to have type UNıT. But I want to allow ſET expressions in‐
side BEGıN expressions, so I permit a subexpression in a BEGıN sequence to have
any type.
Γξ , Γϕ , Γρ ` e1 : τ1 ··· Γξ , Γϕ , Γρ ` en : τn
(BEGıN)
Γξ , Γϕ , Γρ ` BEGıN(e1 , . . . , en ) : τn
The premises mentioning e1 , . . . , en−1 are necessary because although it doesn’t
matter what the types of e1 , . . . , en−1 are, it does matter that they have types.
An empty BEGıN is always well typed and has type UNıT.
(EMPTYBEGıN)
Γξ , Γϕ , Γρ ` BEGıN() : UNıT
A function application is well typed if the function is applied to the right num‐
ber and types of arguments. A function’s type is looked up in the function‐type
environment Γϕ . The type of the application is the result type of the function.
Γϕ (f ) = τ1 × · · · × τn → τ Γξ , Γϕ , Γρ ` ei : τi , 1≤i≤n
(APPLY)
Γξ , Γϕ , Γρ ` APPLY(f, e1 , . . . , en ) : τ
As is typical for a monomorphic language, each polymorphic operation has its
own syntactic form and its own typing rule. An equality test is well typed if it tests
two values of the same type.
Γξ , Γϕ , Γρ ` e 1 : τ Γξ , Γϕ , Γρ ` e2 : τ
(EQ)
Γξ , Γϕ , Γρ ` EQ(e1 , e2 ) : BOOL
A print or println expression is well typed if its subexpression is well typed.
Γξ , Γϕ , Γρ ` e : τ
(PRıNTLN)
Γξ , Γϕ , Γρ ` PRıNTLN(e) : UNıT
Like the corresponding rules of operational semantics, the typing rule for a defi‐
nition may produce type environments with new bindings. The judgment has the
form hd, Γξ , Γϕ i → hΓ′ξ , Γ′ϕ i , which says that when definition d is typed given
type environments Γξ and Γϕ , the new environments are Γ′ξ and Γ′ϕ .
If a variable x has not been bound before, its VAL binding requires only that the
right‐hand side be well typed. The newly bound x takes the type of its right‐hand
side.
Γξ , Γϕ , {} ` e : τ x∈ / dom Γξ
(NEWVAL)
hVAL(x, e), Γξ , Γϕ i → hΓξ {x 7→ τ }, Γϕ i
If x is already bound, the VAL binding acts like a ſET. And just like a ſET, the VAL §6.2
must not change x’s type (Exercise 29). A typechecking
Γξ , Γϕ , {} ` e : τ Γξ (x) = τ interpreter for
(OLDVAL)
hVAL(x, e), Γξ , Γϕ i → hΓξ , Γϕ i Typed Impcore
A top‐level expression is checked, but it doesn’t change the type environments. 337
(In untyped Impcore, the value of a top‐level expression is bound to global vari‐
able it. But in Typed Impcore, the type of it can’t be allowed to change, lest the
type system become unsound. So a top‐level expression doesn’t create a binding.)
Γξ , Γϕ , {} ` e : τ
(EXP)
hEXP(e), Γξ , Γϕ i → hΓξ , Γϕ i
A function definition updates the function environment. The definition gives
the type τi of each formal parameter xi , and the type τ of the result. In an environ‐
ment where each xi has type τi , the function’s body e must be well typed and have
type τ . In that case, function f is added to the type environment Γϕ with function
type τ1 × · · · × τn → τ . Because f could be called recursively from e, f also goes
into the type environment used to typecheck e.
τ1 , . . . , τn are types
τf = τ1 × · · · × τn → τ
f∈ / dom Γϕ
Γξ , Γϕ {f 7→ τf }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ {f 7→ τf }i
(DEFıNE)
In addition to the typing judgment for e, the body of the function, this rule also uses
well‐formedness judgments for types τ1 to τn . In general, when a type appears in
syntax, the rule for that syntax may need a premise saying the type is well formed.
(The result type τ here does not need such a premise, because when the judgment
form · · · ` e : τ is derivable, τ is guaranteed to be well formed; see Exercise 22.)
A function can be redefined, but the redefinition may not change its type (Ex‐
ercise 29).
τ1 , . . . , τn are types
Γϕ (f ) = τ1 × · · · × τn → τ
Γξ , Γϕ {f 7→ τ1 × · · · × τn → τ }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ i
(REDEFıNE)
(LıTERAL)
Γξ , Γϕ , Γρ ` LıTERAL(v) : ıNT
338b. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi≡ (338a) 339a ▷
fun ty (LITERAL v) = INTTY
x ∈ dom Γρ
(FORMALVAR)
Γξ , Γϕ , Γρ ` VAR(x) : Γρ (x)
x∈/ dom Γρ x ∈ dom Γξ
(GLOBALVAR)
Γξ , Γϕ , Γρ ` VAR(x) : Γξ (x)
If x is not found in either Γρ or Γξ , the type checker raises the NotFound exception.
339a. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 338b 339b ▷
| ty (VAR x) = (find (x, formals) handle NotFound _ => find (x, globals))
An assignment (set x e) has a type if both x and e have the same type, in which §6.2
case the assignment has that type. A typechecking
interpreter for
x ∈ dom Γρ Γρ (x) = τ Typed Impcore
Γξ , Γϕ , Γρ ` e : τ
(FORMALAſſıGN) 339
Γξ , Γϕ , Γρ ` ſET(x, e) : τ
x∈
/ dom Γρ x ∈ dom Γξ Γξ (x) = τ
Γξ , Γϕ , Γρ ` e : τ
(GLOBALAſſıGN)
Γξ , Γϕ , Γρ ` ſET(x, e) : τ
The types of both x and e are found by recursive calls to ty.
339b. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 339a 339d ▷
| ty (SET (x, e)) =
let val tau_x = ty (VAR x)
val tau_e = ty e
in if eqType (tau_x, tau_e) then tau_x
else hraise TypeError for an assignment 339ci
end
When x and e have different types—a case not covered by the specification—ty is‐
sues an explanatory error message. Creating this message takes more work than
checking the types.
339c. hraise TypeError for an assignment 339ci≡ (339b)
raise TypeError ("Set variable " ^ x ^ " of type " ^ typeString tau_x ^
" to value of type " ^ typeString tau_e)
A conditional has a type if its condition is Boolean and if both branches have
BOOLTY 331f
the same type—in which case the conditional has that type.
type env 304
eqType 332a
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e 2 : τ Γξ , Γϕ , Γρ ` e3 : τ type exp 332d
(IF)
Γξ , Γϕ , Γρ ` ıF(e1 , e2 , e3 ) : τ find 305b
type funty 331f
Again, most of the code is devoted to error messages. IFX 332d
INTTY 331f
339d. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 339b 340a ▷
LITERAL 332d
| ty (IFX (e1, e2, e3)) = NotFound 305b
let val tau1 = ty e1 SET 332d
val tau2 = ty e2 type ty 331f
val tau3 = ty e3 TypeError S213c
in if eqType (tau1, BOOLTY) then typeString S398e
VAR 332d
if eqType (tau2, tau3) then
tau2
else
raise TypeError
("In if expression, true branch has type " ^
typeString tau2 ^ " but false branch has type " ^
typeString tau3)
else
raise TypeError
("Condition in if expression has type " ^ typeString tau1 ^
", which should be " ^ typeString BOOLTY)
end
A while loop has a type if its condition is Boolean and if its body has a type—we
don’t care what type. In that case the while loop has type unit.
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e2 : τ
(WHıLE)
6 Γξ , Γϕ , Γρ ` WHıLE(e1 , e2 ) : UNıT
340a. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 339d 340b ▷
| ty (WHILEX (e1, e2)) =
let val tau1 = ty e1
Type systems for
val tau2 = ty e2
Impcore and µScheme
in if eqType (tau1, BOOLTY) then
UNITTY
340
else
raise TypeError ("Condition in while expression has type " ^
typeString tau1 ^ ", which should be " ^
typeString BOOLTY)
end
A begin has a type if all its subexpressions have types, in which case the begin
has the type of its last subexpression. Or if there are no subexpressions, type unit.
Γξ , Γϕ , Γρ ` e1 : τ1 ··· Γξ , Γϕ , Γρ ` en : τn
(BEGıN)
Γξ , Γϕ , Γρ ` BEGıN(e1 , . . . , en ) : τn
(EMPTYBEGıN)
Γξ , Γϕ , Γρ ` BEGıN() : UNıT
The implementation uses Standard ML basis function List.last.
340b. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 340a 340c ▷
| ty (BEGIN es) =
let val bodytypes = map ty es
in List.last bodytypes handle Empty => UNITTY
end
An equality test (= e1 e2 ) has a type if e1 and e2 have the same type, in which
case the equality test has type bool.
Γξ , Γϕ , Γρ ` e1 : τ Γξ , Γϕ , Γρ ` e2 : τ
(EQ)
Γξ , Γϕ , Γρ ` EQ(e1 , e2 ) : BOOL
The types of e1 and e2 are computed using an ML trick: val binds the pair of names
(tau1, tau2) to a pair of ML values. This trick has the same effect as the separate
computations of tau1 and tau2 in the WHILEX case above, but it highlights the sim‐
ilarity of the two computations, and it uses scarce vertical space more effectively.
340c. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 340b 340d ▷
| ty (EQ (e1, e2)) =
let val (tau1, tau2) = (ty e1, ty e2)
in if eqType (tau1, tau2) then
BOOLTY
else
raise TypeError ("Equality sees values of different types " ^
typeString tau1 ^ " and " ^ typeString tau2)
end
A print expression has a type if its subexpression is well typed, in which case
its type is unit.
Γξ , Γϕ , Γρ ` e : τ
(PRıNTLN)
Γξ , Γϕ , Γρ ` PRıNTLN(e) : UNıT
340d. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 340c 341a ▷
| ty (PRINTLN e) = (ty e; UNITTY)
| ty (PRINT e) = (ty e; UNITTY)
A function application has a type if its function is defined in environment Γϕ ,
and if the function is applied to the right number and types of arguments. In that
case, the type of the application is the result type of the function type found in Γϕ .
Γϕ (f ) = τ1 × · · · × τn → τ Γξ , Γϕ , Γρ ` ei : τi , 1≤i≤n
(APPLY)
Γξ , Γϕ , Γρ ` APPLY(f, e1 , . . . , en ) : τ
§6.2
If the function is applied to the wrong number or types of arguments, function
A typechecking
badParameter, which is defined in the Supplement, finds one bad parameter and
interpreter for
builds an error message.
Typed Impcore
341a. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ 338bi+≡ (338a) ◁ 340d
341
| ty (APPLY (f, actuals)) = badParameter : int * ty list * ty list ‑> 'a
let val actualtypes = map ty actuals
val FUNTY (formaltypes, resulttype) = find (f, functions)
hdefinition of badParameter S392bi
in if eqTypes (actualtypes, formaltypes) then
resulttype
else
badParameter (1, actualtypes, formaltypes)
end
The typing judgment for a definition d has the form hd, Γξ , Γϕ i → hΓ′ϕ , Γ′ξ i. This
judgment is implemented by a static analysis that I call typing the definition. Calling
typdef (d, Γξ , Γϕ ) returns a triple (Γ′ξ , Γ′ϕ , s), where s is a string describing
a type.
341b. htype checking for Typed Impcore 338ai+≡ (S391a) ◁ 338a
typdef : def * ty env * funty env ‑> ty env * funty env * string
APPLY 332d
fun typdef (d, globals, functions) = badParameter
case d S392b
of hcases for typing definitions in Typed Impcore 341ci BEGIN 332d
bind 305d
Each case of typdef implements one syntactic form of definition d. The first BOOLTY 331f
form is a variable definition, which may change a variable’s value but not its type. type def 333a
Depending on whether the variable is already defined, there are two rules. emptyEnv 305a
type env 304
Γξ , Γϕ , {} ` e : τ x∈ / dom Γξ EQ 332e
(NEWVAL)
hVAL(x, e), Γξ , Γϕ i → hΓξ {x 7→ τ }, Γϕ i eqType 332a
eqTypes 332a
Γξ , Γϕ , {} ` e : τ Γξ (x) = τ find 305b
(OLDVAL) functions 338a
hVAL(x, e), Γξ , Γϕ i → hΓξ , Γϕ i FUNTY 331f
Which rule applies depends on whether x is already defined. type funty 331f
isbound 305c
341c. hcases for typing definitions in Typed Impcore 341ci≡ (341b) 342b ▷
PRINT 332e
VAL (x, e) => PRINTLN 332e
if not (isbound (x, globals)) then type ty 331f
let val tau = typeof (e, globals, functions, emptyEnv) ty 338b
in (bind (x, tau, globals), functions, typeString tau) TypeError S213c
end typeof 338a
typeString S398e
else
UNITTY 331f
let val tau' = find (x, globals)
VAL 333a
val tau = typeof (e, globals, functions, emptyEnv) WHILEX 332d
in if eqType (tau, tau') then
(globals, functions, typeString tau)
else
hraise TypeError with message about redefinition 342ai
end
342a. hraise TypeError with message about redefinition 342ai≡ (341c)
raise TypeError ("Global variable " ^ x ^ " of type " ^ typeString tau' ^
" may not be redefined with type " ^ typeString tau)
A top‐level expression must have a type, and it leaves the environments un‐
6 changed.
Γξ , Γϕ , {} ` e : τ
(EXP)
hEXP(e), Γξ , Γϕ i → hΓξ , Γϕ i
Type systems for 342b. hcases for typing definitions in Typed Impcore 341ci+≡ (341b) ◁ 341c 342c ▷
Impcore and µScheme | EXP e =>
let val tau = typeof (e, globals, functions, emptyEnv)
342 in (globals, functions, typeString tau)
end
τ1 , . . . , τn are types
τf = τ1 × · · · × τn → τ
f∈ / dom Γϕ
Γξ , Γϕ {f 7→ τf }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ {f 7→ τf }i
(DEFıNE)
τ1 , . . . , τn are types
Γϕ (f ) = τ1 × · · · × τn → τ
Γξ , Γϕ {f 7→ τ1 × · · · × τn → τ }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ i
(REDEFıNE)
The common parts of these rules are implemented first: build the function type,
get the type of the body (τ ), and confirm that τ is equal to the returns type in the
syntax. Only then does the code check f ∈ dom Γϕ and finish accordingly.
342c. hcases for typing definitions in Typed Impcore 341ci+≡ (341b) ◁ 342b
| DEFINE (f, {returns, formals, body}) =>
let val (fnames, ftys) = ListPair.unzip formals
val def's_type = FUNTY (ftys, returns)
val functions' = bind (f, def's_type, functions)
val formals = mkEnv (fnames, ftys)
val tau = typeof (body, globals, functions', formals)
in if eqType (tau, returns) then
if not (isbound (f, functions)) then
(globals, functions', funtyString def's_type)
else
let val env's_type = find (f, functions)
in if eqFunty (env's_type, def's_type) then
(globals, functions, funtyString def's_type)
else
raise TypeError
("Function " ^ f ^ " of type " ^ funtyString env's_type
^ " may not be redefined with type " ^
funtyString def's_type)
end
else
raise TypeError ("Body of function has type " ^ typeString tau ^
", which does not match declared result type " ^
"of " ^ typeString returns)
end
6.3 EXTENDıNG TYPED IMPCORE WıTH ARRAYſ
Types are associated with data, and a designer who adds a new data structure must
often add a new type. The process is shown here by adding arrays to Typed Imp‐
core. As shown below, arrays require new types, new concrete syntax, new abstract
syntax, new evaluation code, and new typing rules.
§6.3
Extending
6.3.1 Types for arrays Typed Impcore
with arrays
The array type is a different kind of thing from the integer, Boolean, and UNıT types.
Properly speaking, “array” is not a “type” at all: it is a type constructor, i.e., a thing 343
you use to build types. To name just a few possibilities, you can build arrays of in‐
tegers, arrays of Booleans, and arrays of arrays of integers. In general, given any
type τ , you can build the type “array of τ .” In our abstract syntax, the array type
constructor is represented by ARRAY, and in the ML code, by ARRAYTY (chunk 331f).
In the concrete syntax, it is represented by (array type) (page 330). So for exam‐
ple, the type of arrays of Booleans is (array bool), and the type of arrays of arrays
of integers is (array (array int)).
“Type constructor” is such a useful idea that even ıNT, UNıT, and BOOL are often
treated as type constructors, even though they take no arguments and so can be
used to build only one type apiece. Such nullary type constructors are usually called
base types, because the set of all types is defined by induction and the nullary type
constructors are the base cases.
Whenever you add a new type to a language, whether it is a base type or a more
interesting type constructor, you also add operations for values of that type. Let’s
look at operations for arrays.
Function matrix fills a matrix with zeros; the matrix is updated using syntactic
forms array‑put and array‑at.
344c. htranscript 330i+≡ ◁ 344b
‑> (val a (matrix 3))
[[0 0 0] [0 0 0] [0 0 0]] : (array (array int))
‑> (val i 0)
‑> (val j 0)
‑> (while (< i 3) (begin
(set j 0)
(while (< j 3) (begin
(array‑put (array‑at a i) j (+ i j))
(set j (+ j 1))))
(set i (+ i 1))))
‑> a
[[0 1 2] [1 2 3] [2 3 4]] : (array (array int))
‑> (val a.1 (array‑at a 1))
[1 2 3] : (array int)
‑> (val a.1.1 (array‑at a.1 1))
2 : int
The array‑at form operates on arrays of any type. As shown, it can index into an
array of type (array int) and return a result of type int. It can also index into an
array of type (array (array int)) and return a result of type (array int). Such
behavior is polymorphic. Typed Impcore is monomorphic, which means that a func‐
tion can be used for arguments and results of one and only one type. So like = and
println, array‑at can’t be implemented as a primitive function; it must be a syn‐
tactic form. In fact, every array operation is a syntactic form, defined as follows:
344d. harray extensions to Typed Impcore’s abstract syntax 344di≡ (332d)
| AMAKE of exp * exp
| AAT of exp * exp
| APUT of exp * exp * exp
| ASIZE of exp
This example illustrates a general principle: in a monomorphic language, polymor
phic primitives require specialpurpose abstract syntax. This principle also applies
to C and C++, for example, which denote array operations with syntax involving
square brackets.
Each array operation is governed by rules that say how it is typechecked and
evaluated. Type checking is presented in the next section; evaluation is presented
§6.3
here. Because the evaluation rules are not particularly interesting, they are omitted
Extending
from this book; only the code is shown.
Typed Impcore
Array operations expect arrays and integers, which are obtained from Typed
with arrays
Impcore values by projection (Section 5.2). The projections are implemented by
functions toArray and toInt. If a program type checks, its projections should al‐ 345
ways succeed; if a projection fails, there is a bug in the type checker.
345a. hdefinitions of functions toArray and toInt for Typed Impcore 345ai≡ (S391b)
toArray : value ‑> value array
fun toArray (ARRAY a) = a toInt : value ‑> int
| toArray _ = raise BugInTypeChecking "non‑array value"
fun toInt (NUM n) = n
| toInt _ = raise BugInTypeChecking "non‑integer value"
Given toArray and toInt, the array operations are implemented using the
Array module from ML’s Standard Basis Library. The library includes run‐time
checks for bad subscripts or array sizes; these checks are needed because Typed
Impcore’s type system is not powerful enough to preclude such errors.
345b. hmore alternatives for ev for Typed Impcore 345bi≡ (S397b)
| ev (AAT (a, i)) = ev : exp ‑> value
Array.sub (toArray (ev a), toInt (ev i))
| ev (APUT (e1, e2, e3)) =
let val (a, i, v) = (ev e1, ev e2, ev e3)
in Array.update (toArray a, toInt i, v);
v
end
| ev (AMAKE (len, init)) =
ARRAY (Array.array (toInt (ev len), ev init))
| ev (ASIZE a) =
NUM (Array.length (toArray (ev a)))
In a monomorphic language, a new type constructor needs new syntax, and new ARRAY 332c
syntactic forms need new typing rules. Like the operations described in Sec‐ BugInTypeChecking
tion 2.5.2, syntax should include forms that create and observe, as well as forms S213c
that produce or mutate, or both. Rules have similar requirements. ev S397b
type exp 332d
Rules should say how to use the new type constructor to make new types. Such NUM 332c
rules are called formation rules. Rules should also say what syntactic forms create type value 332c
new values that are described by the new type constructor. Such rules are called
introduction rules; an introduction rule describes a syntactic form that is analo‐
gous to a creator function as described in Section 2.5.2. Finally, rules should say
what syntactic forms use the values that are described by the new type constructor.
Such rules are called elimination rules; an elimination rule describes a syntactic
form that may be analogous to a mutator or observer function as described in Sec‐
tion 2.5.2.
A rule can be recognized as a formation, introduction, or elimination rule by
first drawing a box around the type of interest, then seeing if the rule matches any
of these templates:
6 • A formation rule answers the question, “what types can I make?” Below the
line, it has the judgment “ is a type,” where is a type of interest:
··· . (FORMATıON TEMPLATE)
Type systems for is a type
Impcore and µScheme
• An introduction rule answers the question, “how do I make a value of the
346 interesting type?” Below the line, it has a typing judgment that ascribes the
type of interest to an expression whose form is somehow related to the type.
To write that expression, I use a ? mark:
··· . (INTRODUCTıON TEMPLATE)
Γ`?:
• An elimination rule answers the question, “what can I do with a value of the
interesting type?” Above the line, it has a typing judgment that ascribes the
type of interest to an expression whose form is unknown. Such an expression
will be written as e, e1 , e′ , or something similar:
··· Γ`e: · · ·.
(ELıMıNATıON TEMPLATE)
···
These templates work perfectly with the formation, introduction, and elimina‐
tion rules for arrays. To start, an array type is formed by supplying the type of its
elements. In Typed Impcore, the length of an array is not part of its type.2
τ is a type
(ARRAYFORMATıON)
ARRAY(τ ) is a type
Γξ , Γϕ , Γρ ` e1 : ıNT Γξ , Γϕ , Γρ ` e2 : τ
(MAĸEARRAY)
Γξ , Γϕ , Γρ ` MAĸE‐ARRAY(e1 , e2 ) : ARRAY(τ )
The MAĸE‐ARRAY form is definitely related to the array type, and this rule matches
the introduction template.
An array is used by indexing it, updating it, or taking its length. Each operation
is described by an elimination rule.
Γξ , Γϕ , Γρ ` e1 : ARRAY(τ ) Γξ , Γϕ , Γρ ` e2 : ıNT
(ARRAYAT)
Γξ , Γϕ , Γρ ` ARRAY‐AT(e1 , e2 ) : τ
Γξ , Γϕ , Γρ ` e1 : ARRAY(τ ) Γξ , Γϕ , Γρ ` e2 : ıNT Γξ , Γϕ , Γρ ` e3 : τ
Γξ , Γϕ , Γρ ` ARRAY‐PUT(e1 , e2 , e3 ) : τ
(ARRAYPUT)
Γξ , Γϕ , Γρ ` e : ARRAY(τ )
(ARRAYSıZE)
Γξ , Γϕ , Γρ ` ARRAY‐ſıZE(e) : ıNT
2
Because the length of an array is not part of its type, Typed Impcore requires a run‐time safety check
for every array access. Such checks can be eliminated by a type system in which the length of an array
is part of its type (Xi and Pfenning 1998), but these sorts of type systems are beyond the scope of this
book.
Understanding formation, introduction, and elimination
Not all syntactically correct types and expressions are acceptable in programs;
acceptability is determined by formation, introduction, and elimination rules.
Formation rules tell us what types are acceptable; for example, in Typed Imp‐ §6.3
core, int, bool, and (array int) are acceptable types, but (int bool), array, Extending
and (array array) are not. In C, unsigned and unsigned* are acceptable, but Typed Impcore
* and *unsigned are not. Type‐formation rules are usually easy to write. with arrays
Introduction and elimination rules tell us what terms (expressions) are accept‐ 347
able. The words “introduction” and “elimination” come from formal logic; the
ideas they represent have been adopted into programming languages via the
principle of propositions as types. This principle says that a type constructor cor‐
responds to a logical connective, a type corresponds to a proposition, and a term
of the given type corresponds to a proof of the proposition. For example, logical
implication corresponds to the function arrow; if type τ corresponds to propo‐
sition P , then the type τ → τ corresponds to the proposition “P implies P ”;
and the identity function of type τ → τ corresponds to a proof of “P implies P .”
A term may inhabit a particular type “directly” or “indirectly.” Which is which
depends on how the term relates to the type constructor used to make the
type. For example, a term of type (array bool) might refer to the variable
truth‑vector, might evaluate a conditional that returns an array, or might ap‐
ply a function that returns an array. All these forms are indirect: variable ref‐
erence, conditionals, and function application can produce results of any type
and are not specific to arrays. But if the term has the make‑array form, it builds
an array directly; a make‑array term always produces an array. Similar direct
and indirect options are available in proofs.
In both type theory and logic, the direct forms are the introduction forms, and
their acceptable usage is described by introduction rules. The indirect forms
are typically elimination forms, described by elimination rules. For example,
the conditional is an elimination form for Booleans, and function application is
an elimination form for functions. In general, an introduction form puts new
information into a proof or a term, whereas an elimination form extracts in‐
formation that was put there by an introduction form. For example, if I get a
Boolean using array‑at, I’m getting information that was in the array, so I’m
using an elimination form for arrays, not an introduction form for Booleans.
An introduction or elimination form is a syntactic form of expression, and it is
associated with a type (or type constructor) that appears in its typing rule. An in‐
troduction form for a type constructor µ will have a typing rule that has a µ type
in the conclusion; types in premises are often arbitrary types τ . An elimination
form for the same µ will have a typing rule that has a µ type in a premise; the
type in the conclusion is often an arbitrary type τ , or sometimes a fixed type
like bool. Identifying introduction and elimination forms is the topic of Exer‐
cise 2 (page 388).
In a good design, information created by any introduction form can be extracted
by an elimination form, and vice versa. A design that has all the forms and rules
it needs resembles as design that has all the algebraic laws it needs, as discussed
in Section 2.5.2 (page 111). Introduction forms relate to algebraic laws’ creators
and producers, and elimination forms relate to observers.
In each of these rules, an expression of array type (e1 or e) has an arbitrary syntac‐
tic form, which need not be related to arrays. These rules match the elimination
template.
These rules can be turned into code for the type checker, which you can fill in
6 (Exercise 18):
348. hfunction ty, checks type of expression given Γξ , Γϕ , Γρ [[prototype]] 348i≡
| ty (AAT (a, i)) = raise LeftAsExercise "AAT"
| ty (APUT (a, i, e)) = raise LeftAsExercise "APUT"
Type systems for
| ty (AMAKE (len, init)) = raise LeftAsExercise "AMAKE"
Impcore and µScheme
| ty (ASIZE a) = raise LeftAsExercise "ASIZE"
348
6.4 COMMON TYPE CONſTRUCTORſ
Arrays are just one type of data. Others include functions, products, sums, and mu‐
table references. All these types have proven their worth in many languages, and
they all have standard typing rules, which are described in this section. The rules
use a single type environment Γ, which replaces the triple Γξ , Γϕ , Γρ used in Typed
Impcore.
Functions If functions are first‐class values, they should have first‐class types. The
function type constructor, which takes two arguments, is written using an infix →.
Functions are introduced by λ‐abstraction and eliminated by function application.
The λ‐abstraction I show here makes the types of the formal parameters explicit,
like a function definition in Typed Impcore. And for simplicity, it takes just one
parameter.
τ1 and τ2 are types
(ARROWFORMATıON)
τ1 → τ2 is a type
Γ{x 7→ τ } ` e : τ ′
(ARROWINTRO)
Γ ` LAMBDA(x : τ, e) : τ → τ ′
Γ ` e1 : τ → τ ′ Γ ` e2 : τ
(ARROWELıM)
Γ ` APPLY(e1 , e2 ) : τ ′
Products A product, often called a pair or tuple, groups together values of different
types. It corresponds to ML’s “tuple” type, to C’s “struct,” to Pascal’s “record,” and
to the “Cartesian product” you may remember from math class. It is written us‐
ing an infix ×. (To motivate the word “product,” think about counting inhabitants:
if two values inhabit type bool and five values inhabit type lettergrade, how many
values inhabit product type bool × lettergrade?)
In addition to a formation rule, product types are supported by one introduction
form, PAıR, and two elimination forms, FſT and ſND.
Γ ` e1 : τ1 Γ ` e2 : τ2
(PAıRINTRO)
Γ ` PAıR(e1 , e2 ) : τ1 × τ2
Γ ` e : τ1 × τ2
(FſT)
Γ ` FſT(e) : τ1
Γ ` e : τ1 × τ2
(SND)
Γ ` ſND(e) : τ2
Like array operations, the pair operations are polymorphic, i.e., they can work with
pairs of any types. And the pair operations are familiar: they appear in Chapter 2
under the names cons, car, and cdr. As noted in that chapter, their dynamic se‐
mantics is given by these algebraic laws:
FſT(PAıR(v1 , v2 )) = v1
ſND(PAıR(v1 , v2 )) = v2
Pair operations can be written using other notations. Often PAıR(e1 , e2 ) is writ‐
§6.4
ten in concrete syntax as (e1 , e2 ). Syntactic forms FſT and ſND may be written to
Common type
look like functions fst and snd. In ML, these forms are written #1 and #2; in math‐
constructors
ematical notation, they are sometimes written π1 and π2 . Or they may be written
using postfix notation; for example, FſT(e) might be written as e.1. 349
Using FſT and ſND to get the elements of a pair can be awkward. In ML code,
there is a better idiom: pattern matching, which is combines elimination and bind‐
ing. The ML pattern match let val (x, y ) = e′ in e end can be given a type as fol‐
lows:
Γ ` e′ : τ1 × τ2
Γ{x 7→ τ1 , y 7→ τ2 } ` e : τ . (LETPAıR)
Γ ` let val (x, y ) = e′ in e end : τ
The pair rules can be generalized to give types to tuples with any number of
elements—even zero! The type of tuples with zero elements can serve as a UNıT
type, since it is inhabited by only one value: the empty tuple. The tuple rules can
be further generalized to give a name to each element of a tuple, so elements can be
referred to by name instead of by position, something like a C struct (Exercise 5).
Sums Where a product provides an ordered collection of values of different types,
a sum provides a choice among values of different types. And where products are
supported in similar, obvious ways in almost every language, sums are supported
in more diverse ways, some of which are hard to recognize. Easily recognizable
examples include C’s “union” types and Pascal’s “variant records.” ML’s datatype is
also a form of sum type.
In type theory, a sum type is written τ1 + τ2 . A value of type τ1 + τ2 is either
a value of type τ1 or a value of type τ2 , tagged or labeled in a way that lets you
tell which is which. (To motivate the word “sum,” count the inhabitants of type
bool + lettergrade. If you’re not sure about counting inhabitants, do Exercise 3.)
In addition to a formation rule, sum types are supported by two introduction forms:
Sum types are supported by a single elimination form: case, which in some lan‐
guages is called switch. Let the let pattern match above, a case expression has
too many parts to be understood when written in abstract‐syntax notation; the rule
uses ML‐like concrete syntax.
Γ ` e : τ1 + τ2
Γ{x1 7→ τ1 } ` e1 : τ
Γ{x2 7→ τ2 } ` e2 : τ
(SUMELıMCAſE)
Γ ` case e of LEFT(x1 ) ⇒ e1 | RıGHT(x2 ) ⇒ e2 : τ
Sums and case expressions behave as described by these rules of operational
semantics, which use (unsubscripted) value forms LEFT(v) and RıGHT(v):
(LEFT) (RıGHT)
he, ρ, σi ⇓ hv, σ ′ i he, ρ, σi ⇓ hv, σ ′ i
6 hLEFTτ (e), ρ, σi ⇓ hLEFT(v), σ ′ i hRıGHTτ (e), ρ, σi ⇓ hRıGHT(v), σ ′ i
,
he, ρ, σi ⇓ hLEFT(v1 ), σ ′ i
Type systems for / dom σ ′
ℓ1 ∈
Impcore and µScheme he1 , ρ{x1 7→ ℓ1 }, σ ′ {ℓ1 7→ v1 }i ⇓ hv, σ ′′ i , (CAſELEFT)
hcase e of LEFT(x1 ) ⇒ e1 | RıGHT(x2 ) ⇒ e2 , ρ, σi ⇓ hv, σ ′′ i
350
he, ρ, σi ⇓ hRıGHT(v2 ), σ ′ i
/ dom σ ′
ℓ2 ∈
he2 , ρ{x2 7→ ℓ2 }, σ ′ {ℓ2 7→ v2 }i ⇓ hv, σ ′′ i . (CAſERıGHT)
hcase e of LEFT(x1 ) ⇒ e1 | RıGHT(x2 ) ⇒ e2 , ρ, σi ⇓ hv, σ ′′ i
If a language has first‐class functions, the case expression can be replaced with
a simpler form, either. The expression (either e f g ), which is equivalent to the
case expression case e of LEFT(x1 ) ⇒ f (x1 ) | RıGHT(x2 ) ⇒ g(x2 ), applies
either f or g to the value “carried” inside e.
Just like products, sums can be generalized so that each alternative has a name
(Exercise 6).
Mutable cells The types shown above are all immutable, meaning that once cre‐
ated, a value can’t be changed. Since mutation is a useful programming tech‐
nique, essential for both procedural and object‐oriented programming, type sys‐
tems should support mutability—for example, with a type for “mutable cell con‐
taining value of type τ ” (Exercise 7).
If a program is well typed, what does that imply? It depends on the type system.
A static type system should guarantee some properties about programs—usually
safety properties. In other words, if a program type checks at compile time, that
should tell you something about the program’s behavior at run time.
A serious type system is designed around a safety property and supported by
a proof that “well‐typed programs don’t go wrong,” i.e., that well‐typed programs
satisfy the safety property. The type systems in this book can guarantee such prop‐
erties as “the program never attempts to take car of an integer” or “a function is
always called with the correct number of arguments.” More advanced type sys‐
tems can guarantee such properties as “no array access is ever out of bounds,”
“no pointer ever refers to memory that has been deallocated,” or “no private in‐
formation is ever stored in a public variable.” Whatever the property of interest,
the proof that a type system guarantees it is a soundness result.
A precise claim about soundness might refer to the intended meaning of each
type. One common meaning is that a type τ prescribes a set of values [[τ ]]. For ex‐
ample,
A simple soundness claim might say that an expression of type τ evaluates to a value
in the set [[τ ]]. The claim is subject to conditions: evaluation must terminate, and
evaluation and type checking must be done in compatible environments. A prop‐
erly phrased claim might read like this:
That is, if the environments make sense, and if expression e has type τ , and if eval‐ §6.6
uating e produces a value, then evaluation produces a value in [[τ ]]. (To simplify Polymorphic type
the statement of the claim, I’ve used the simplest possible operational semantics, systems and Typed
which uses no store.) A claim like this could be proved by simultaneous induction µScheme
on the structure of the proof of Γ ` e : τ and the proof of hρ, ei ⇓ v .
An even stronger soundness claim might add that unless a primitive like / or car 351
fails, or unless e loops forever, that the evaluation of e does indeed produce a value.
Using a small‐step semantics like the one described in Chapter 3, such a claim
might say that when all the expressions in an abstract machine are well typed, the
machine has either terminated with a value and an empty stack, or it can step to a
new state in which all its expressions are still well typed.
A type system with a strong soundness claim rules out most run‐time errors.
For example, in a sound, typed, Scheme‐like language, then if evaluating e does
not attempt to divide by zero or take car or cdr of the empty list, and if evaluating e
doesn’t get into an infinite loop, then the evaluation of e completes successfully.
The full benefits of types aren’t provided by Typed Impcore, which is both too com‐
plicated and not powerful enough. Typed Impcore is too complicated because of
its multiple type environments Γξ , Γϕ , and Γρ . It is not powerful enough because
each operation that works with values of more than one type, like = or println, has
to be built into its abstract syntax. A function defined by a user can operate only
on values of a single type, which is to say it is monomorphic. For example, a user
can’t define a reusable array‐reversal function that could operate on both an array
of Booleans and an array of integers. This limitation is shared by such languages
as C and Pascal.
Monomorphism handicaps programmers. Many primitive structures, includ‐
ing arrays, lists, tables, pointers, products, sums, and objects, inherently work
with multiple types: they are polymorphic. But when user‐defined functions are
monomorphic, a computation like the length of a list has to be coded anew for
each type of list element, as in Chapter 1.
And monomorphic languages are hard to extend with new type constructors.
A language designer can do it, provided they are willing to add new rules to a type
system and to revisit its proof of type soundness. But a programmer can’t; unless
there is some sort of template or macro system, no programmer can add a user‐
defined, polymorphic type constructor such as the env type constructor used for
environments in Chapters 5 to 10. At best, a programmer can add a new base type,
not a new type constructor.
These problems are solved by polymorphic type systems. Such type systems en‐
able a programmer to write polymorphic functions and to add new type construc‐
tors. Our first polymorphic type system is part of a language called Typed micro
Scheme, or Typed µScheme for short. Typed µScheme is patterned after µScheme:
it uses the same values as µScheme and similar abstract syntax.
Our study of Typed µScheme begins with concrete syntax. It continues with
kinds and quantified types; these are the two ideas at the core of the type system.
Kinds are used to ensure that every type written in the source code is well formed
and meaningful; kinds classify types in much the same way that types classify
terms. Quantified types express polymorphism; they make it possible to imple‐
ment polymorphic operations using ordinary functions instead of special‐purpose
abstract syntax. Building on these ideas, the rest of the chapter presents techni‐
cal details needed to make a polymorphic type system work: type equivalence and
6 substitution. Type equivalence is a relation that shows when two types cannot be
distinguished by any program, even if they don’t look identical. And substitution is
the mechanism by which a polymorphic value is instantiated so it can be used.
Type systems for
Impcore and µScheme 6.6.1 Concrete syntax of Typed µScheme
• All letrec expressions require type annotations for bound names—and each
name may be bound only to a lambda abstraction.
• Instead of µScheme’s single val form, Typed µScheme provides two forms:
val‑rec, which is recursive and defines only functions, and val, which is
non‐recursive and can define any type of value. Only the val‑rec form re‐
quires a type annotation. Typed µScheme’s val and val‑rec forms resemble
the corresponding forms in Standard ML.
The type system of Typed µScheme is more powerful than that of Typed Impcore:
• Typed µScheme adds quantified types, which are written with forall. Val‐
ues of quantified type are introduced by a new syntactic form of expression:
type‑lambda. They are eliminated by the new syntactic form @.
• Syntactically, Typed µScheme does not distinguish a “type” from a “type con‐
structor”; both can be called “types,” and both are in the syntactic category
typeexp. The category, which is called “type‐level expression,” also includes
ill‐formed nonsense that is neither type nor type constructor, like (int int).
• In Typed µScheme, only the type constructor for functions requires special‐
purpose syntax; a function is introduced by lambda and eliminated by func‐
tion application. Other type constructors, like pairs and arrays, require no
new syntax or new typing rules. They go into the initial basis, where their
operations are implemented as ordinary (primitive) functions.
The syntax of Typed µScheme is shown in Figure 6.3 on the next page.
Types in source code are written by programmers, and they can’t be trusted.
“Types” like (int int) are ill formed and must be rejected. In Typed Impcore,
types are determined to be well formed or ill formed by type‐formation rules. And
for a language with a fixed set of types and type constructors, that’s fine. But in
Typed µScheme, we want to be able to add new type constructors without adding
new rules. So Typed µScheme uses just a few rules to encompass arbitrarily many
type constructors. The rules rely on each type constructor having a kind.
Kinds classify types (and type constructors) in much the same way that types
classify terms. A kind shows how a type constructor may be used. For example,
def ::= (val variablename exp)
| (val‑rec [variablename : typeexp] exp) §6.6.2
| (define typeexp functionname (formals) exp) A replacement for
| exp typeformation
| (use filename) rules: Kinds
| unittest
353
unittest ::= (check‑expect exp exp)
| (check‑assert exp)
| (check‑error exp)
| (check‑type exp typeexp)
| (check‑type‑error def )
exp ::= literal
| variablename
| (set variablename exp)
| (if exp exp exp)
| (while exp
exp)
| (begin exp )
| (exp exp )
| (letkeyword
( [variablename exp] ) exp)
| (letrec [ ([variablename : typeexp] exp) ] exp)
| (lambda (formals) exp)
| (type‑lambda
[typeformals] exp)
| [@ exp typeexp ]
letkeyword ::= let let*
formals ::= [variablename : typeexp]
typeformals ::= 'typevariablename
numeral ::= token composed only of digits, possibly prefixed with a plus
or minus sign
*name ::= token that is not a bracket, a numeral, or one of the “re‐
served” words shown in typewriter font
Types that are inhabited by values, like int or (list bool), have kind ∗. Types of
other kinds, like list and array, are ultimately used to make types of kind ∗.
Some common kinds, with example type constructors of those kinds, are as
follows:
∗ int, bool, unit
∗⇒∗ list, array, option
∗×∗⇒∗ pair, sum, Standard ML’s ‑>
More exotic kinds can be found in languages like Haskell, which includes not only
“monads,” which are all types of kind ∗ ⇒ ∗, but also “monad transformers,” which
are types of kind (∗ ⇒ ∗) ⇒ (∗ ⇒ ∗).
Every syntactically expressible kind κ is well formed:
, (KıNDFORMATıONTYPE)
∗ is a kind
How do we know which type constructors have which kinds? The kind of
each type constructor is stored in a kind environment, written ∆. The exam‐
ple environment ∆0 below shows the kinds of the primitive type constructors of
Typed µScheme. Each binding is written using the :: symbol, which is used instead
of 7→; it is pronounced “has kind.”
The kind environment determines how both int and array may be used. New
type constructors can be added to Typed µScheme just by adding them to ∆0 (Ex‐
ercises 10 to 13).
A kind environment is used to tell what types are well formed. No matter
how many type constructors are defined, they are handled using just three type‐
formation rules:
µ ∈ dom ∆
(KıNDINTROCON)
∆ ` TYCON(µ) :: ∆(µ)
∆ ` τ :: κ1 × · · · × κn ⇒ κ ∆ ` τi :: κi , 1 ≤ i ≤ n
(KıNDAPP)
∆ ` CONAPP(τ, [τ1 , . . . , τn ]) :: κ
∆ ` τi :: ∗, 1 ≤ i ≤ n ∆ ` τ :: ∗
(KıNDFUNCTıON)
∆ ` τ1 × · · · × τn → τ :: ∗
No matter how many type constructors we may add to Typed µScheme, these kind‐
ing rules tell us everything we will ever need to know about the formation of
types. Compare this situation with the situation in Typed Impcore. In Typed Imp‐
core, we need the BAſETYPEſ rule for int and bool. To add arrays we need the
ARRAYFORMATıON rule. To add lists we would need a list‐formation rule (Exer‐
cise 4, page 389). And so on. Unlike Typed Impcore’s type system, Typed µScheme’s
type system can easily be extended with new type constructors (Exercises 10 to 13).
Similar ideas are used in languages in which programmers can define new type con‐
structors, including µML and Molecule (Chapters 8 and 9).
Implementing kinds
6 ("bool", TYPE) ::
("sym", TYPE) ::
("unit", TYPE) ::
("list", ARROW ([TYPE], TYPE)) ::
Type systems for
The kind system and the type‐formation rules shown above replace the type‐
Impcore and µScheme
formation rules of Typed Impcore. To get polymorphism, however, we need some‐
356 thing more: quantified types.
Suppose length could be defined in Typed Impcore; what would its type be? In a
monomorphic language like Typed Impcore or C, a function can have at most one
type, so the definition would have to designate an element type. To use length with
different types of lists would require different versions:
(define int lengthI ([xs : (list int)])
(if (null? xs) 0 (+ 1 (lengthI (cdr xs)))))
(define int lengthB ([xs : (list bool)])
(if (null? xs) 0 (+ 1 (lengthB (cdr xs)))))
(define int lengthS ([xs : (list sym)])
(if (null? xs) 0 (+ 1 (lengthS (cdr xs)))))
Such duplication wastes effort; except for the types, the functions are identical. but
Typed Impcore’s type system cannot express the idea that length works with any
list, independent of the element type. To express the idea that length could work
with any element type, we need type variables and quantified types.
A type variable stands for an unknown type; a quantified type grants permis‐
sion to substitute any type for a type variable. In this book, type variables are written
using the Greek letters α, β , and γ ; quantified types are written using ∀. For ex‐
ample, the type of a polymorphic length function is ∀α . α list → int. Greek
letters and math symbols can be awkward in code, so in Typed µScheme this type
is written (forall ['a] ((list 'a) ‑> int)).
A forall type is not a function type; the length function can’t be used on a list
of Booleans, for example, until it is instantiated. The instantiation (@ length bool)
strips “∀α.” from the front of length’s type, and in what remains, substitutes bool
for α. The type of the resulting instance is bool list → bool, or in Typed µScheme,
((list bool) ‑> int). This instance can be applied to a list of Booleans.
Like lambda, ∀ is a binding construct, and the variable α is sometimes called a
type parameter. Like the name of a formal parameter, the name of a type parameter
doesn’t matter; for example, the type of the length function could also be written
∀β . β list → int, and its meaning would be unchanged. That’s because the
meaning of a quantified type is determined by how it behaves when we strip the
quantifier and substitute for the bound type variable.
In abstract syntax, type variables and quantified types are written using TYVAR
and FORALL. And like TYCON and CONAPP, TYVAR and FORALL are governed by
kinding rules. (The kind system replaces the type‐formation rules used in Typed
Impcore; remember the slogan “just as types classify terms, kinds classify types.”)
The kind of a type variable, like the kind of a type constructor, is looked up in
the environment ∆.
§6.6.3
α ∈ dom ∆ The heart of
(KıNDINTROVAR) polymorphism:
∆ ` TYVAR(α) :: ∆(α)
Quantified types
The kind of a quantified type is always ∗, and the FORALL quantifier may be used
357
only over types of kind ∗. Within the body of the FORALL, the quantified variables
stand for types. So above the line, they are introduced into the kind environment
with kind ∗.
∆{α1 :: ∗, . . . , αn :: ∗} ` τ :: ∗
(KıNDALL)
∆ ` FORALL(hα1 , . . . , αn i, τ ) :: ∗
In some polymorphic type systems, including the functional language Haskell, type
variables may have other kinds.
In Typed µScheme, every type is written using a type‐level expression (nonter‐
minal typeexp in Figure 6.3, page 353). In the interpreter, a type‐level expression
is represented by a value of the ML type tyex; its forms include not only TYVAR and
FORALL but also TYCON, CONAPP, and a function‐type form.
357a. htypes for Typed µScheme 357ai≡ (S405a) 381a ▷
datatype tyex = TYCON of name (* type constructor *)
| CONAPP of tyex * tyex list (* type‑level application *)
| FUNTY of tyex list * tyex (* function type *)
| FORALL of name list * tyex (* quantified type *)
| TYVAR of name (* type variable *)
Even though not every tyex represents a well‐formed type, it’s easier to call them
all “types”—except when we have to be careful.
Examples of well‐formed types, written using concrete syntax, include the
types of the following polymorphic functions and values related to lists:
357b. htranscript 357bi≡ 358a ▷
‑> length
<function> : (forall ['a] ((list 'a) ‑> int))
‑> cons
<function> : (forall ['a] ('a (list 'a) ‑> (list 'a)))
‑> car
<function> : (forall ['a] ((list 'a) ‑> 'a))
‑> cdr
<function> : (forall ['a] ((list 'a) ‑> (list 'a)))
‑> '()
ARROW 355a
() : (forall ['a] (list 'a)) type name 303
Polymorphism is not restricted to functions: even though it is not a function, the TYPE 355a
In each case, the type of the instance is obtained by substituting each type parame‐
ter for the corresponding type variable in the forall. Each instance is monomor‐
phic, and if it has an arrow type, it can be applied to values.
358b. htranscript 357bi+≡ ◁ 358a 358c ▷
‑> (length‑at‑int '(1 4 9 16 25))
5 : int
‑> (cons‑at‑bool #t '(#f #f))
(#t #f #f) : (list bool)
‑> (car‑at‑pair ([@ cons (pair sym int)]
([@ pair sym int] 'Office 231)
[@ '() (pair sym int)]))
(Office . 231) : (pair sym int)
‑> (cdr‑at‑sym '(a b c d))
(b c d) : (list sym)
Getting the instances you want takes thought and practice. A common mistake
is to instantiate by substituting the type you want the instance to have. If you want a
function of type ((list bool) ‑> int), instantiate length at bool. If instead you in‐
stantiate length at the desired type ((list bool) ‑> int), the instance won’t have
the type you hoped for:
358c. htranscript 357bi+≡ ◁ 358b 359a ▷
‑> (val useless‑length [@ length ((list bool) ‑> int)])
useless‑length : ((list ((list bool) ‑> int)) ‑> int)
4
Instantiation is also called type application. It is deeply related to function application. Instantiation
is defined by substituting for type variables bound by ∀. And in Alonzo Church’s fundamental theory of
programming languages, the lambda calculus, function application is defined by substituting for term
variables bound by λ.
A function like useless‑length has a good type but can’t be used to take the
length of a list of Booleans.
359a. htranscript 357bi+≡ ◁ 358c 362 ▷
‑> (useless‑length '(#t #f #f))
type error: function useless‑length of type ...
‑> [@ length bool]
§6.6.3
<function> : ((list bool) ‑> int)
‑> ([@ length bool] '(#t #f #f))
The heart of
3 : int polymorphism:
Quantified types
As the car‑at‑pair example and the final two length examples show, an instance
doesn’t have to be named; instances can be used directly. 359
The instantiation form @ lets you use a polymorphic value; it is the elimination
form for quantified types. To create a polymorphic value you need an introduction
form. In Typed µScheme, the introduction form is written using type‑lambda; it is
sometimes called type abstraction. As an example, I use type‑lambda to define the
polymorphic functions list1, list2, and list3.
359b. hpredefined Typed µScheme functions 359bi≡ 359c ▷
(val list1 (type‑lambda ['a] (lambda ([x : 'a])
([@ cons 'a] x [@ '() 'a]))))
(val list2 (type‑lambda ['a] (lambda ([x : 'a] [y : 'a])
([@ cons 'a] x ([@ list1 'a] y)))))
(val list3 (type‑lambda ['a] (lambda ([x : 'a] [y : 'a] [z : 'a])
([@ cons 'a] x ([@ list2 'a] y z)))))
Other higher‐order functions are not only polymorphic but also recursive.
Such functions are defined by nesting letrec (for recursion) inside type‑lambda
(for polymorphism).
359d. hpredefined Typed µScheme functions 359bi+≡ ◁ 359c 360a ▷
(val length
(type‑lambda ['a]
(letrec
[([length‑mono : ((list 'a) ‑> int)]
(lambda ([xs : (list 'a)])
(if ([@ null? 'a] xs)
0
(+ 1 (length‑mono ([@ cdr 'a] xs))))))]
length‑mono)))
The inner function is called length‑mono because it—like any value introduced with
lambda—is monomorphic, operating only on lists of the given element type 'a: the
recursive call to length‑mono does not require an instantiation.
Every polymorphic, recursive function is defined using the same pattern: val
6 to type‑lambda to letrec. Another example is an explicitly typed version of the
reverse‐append function:
360a. hpredefined Typed µScheme functions 359bi+≡ ◁ 359d 360b ▷
(val revapp
Type systems for
(type‑lambda ['a]
Impcore and µScheme
(letrec [([revapp‑mono : ((list 'a) (list 'a) ‑> (list 'a))]
Like the concrete syntax, the abstract syntax of Typed µScheme resembles the ab‐
stract syntax of µScheme. Typed µScheme adds two new expressions, TYLAMBDA
and TYAPPLY, which introduce and eliminate quantified types. And it requires that
§6.6.4
names bound by letrec or lambda (internal recursive functions and the parame‐
Abstract syntax,
ters of every function) be annotated with explicit types.
values, and
361a. hdefinitions of exp and value for Typed µScheme 361ai≡ (S405b) 361b ▷
evaluation of
datatype exp = LITERAL of value
Typed µScheme
| VAR of name
| SET of name * exp 361
| IFX of exp * exp * exp
| WHILEX of exp * exp
| BEGIN of exp list
| APPLY of exp * exp list
| LETX of let_flavor * (name * exp) list * exp
| LETRECX of ((name * tyex) * exp) list * exp
| LAMBDA of lambda_exp
| TYLAMBDA of name list * exp
| TYAPPLY of exp * tyex list
and let_flavor = LET | LETSTAR
The values of Typed µScheme are the same as the values of µScheme; adding a type
system doesn’t change the representation used at run time.
361b. hdefinitions of exp and value for Typed µScheme 361ai+≡ (S405b) ◁ 361a
and value = NIL
| BOOLV of bool
| NUM of int
| SYM of name
| PAIR of value * value
| CLOSURE of lambda_value * value ref env
| PRIMITIVE of primitive
withtype primitive = value list ‑> value (* raises RuntimeError *)
and lambda_exp = (name * tyex) list * exp
and lambda_value = name list * exp
The definitions of Typed µScheme are like those of Typed Impcore, plus the
recursive binding form VALREC (see sidebar on the following page).
361c. hdefinition of def for Typed µScheme 361ci≡ (S405b)
datatype def = VAL of name * exp
| VALREC of name * tyex * exp
| EXP of exp
| DEFINE of name * tyex * lambda_exp
• There is a kind environment, ∆, which keeps track of the kinds of type vari‐
ables and type constructors.
Like µScheme, Typed µScheme has more definition forms that it really needs:
6 given its lambda and letrec expressions, the only definition form it really needs
is val (Exercise 21). But Typed µScheme is not meant to be as small as possible;
it’s meant to convey understanding and to facilitate comparisons. So it includes
define.
Type systems for
In untyped µScheme, define is just syntactic sugar for a val binding to a lambda
Impcore and µScheme
(page 120). Because a µScheme val makes its bound name visible on the right‐
362 hand side, such functions can even be recursive. µScheme’s operational seman‐
tics initializes the name to an unspecified value, then overwrites the name with
a closure. This semantics extends to all val bindings; for example, in untyped
µScheme you can write (val n (+ n 1)), and on the right‐hand side, the value
of n is unspecified. But in Typed µScheme, we can’t afford to compute with un‐
specified values; if an expression like (+ n 1) typechecks, we have to know that
n has an integer value.
Typed µScheme works around this problem by changing the operational se‐
mantics of val back to the semantics used in Impcore: in Typed µScheme, as
in Impcore, a val binding is not recursive, and the name being defined is not
visible on the right‐hand side.
362. htranscript 357bi+≡ ◁ 359a 368a ▷
‑> (val n (+ n 1))
Name n not found
For recursive bindings, Typed µScheme introduces the new form val‑rec,
which has the same operational semantics as µScheme’s val form. To en‐
sure that the right‐hand side does not evaluate the name before it is initialized,
Typed µScheme restricts the right‐hand side to be a lambda form. And to make
it possible to typecheck recursive calls, Typed µScheme requires a type annota‐
tion that gives the type of the bound name. Using val‑rec and lambda, define
can easily be expressed as syntactic sugar.
The distinction between val and val‑rec can be found in other languages. Look
for it! For example, in C, type definitions act like val, but in Modula‐3, they act
like val‑rec. And in Haskell, every definition form acts like val‑rec! (Haskell
gets away with this because no definition form evaluates its right‐hand side.)
• In a forall type, the names of quantified type variables are not supposed
to matter. This detail affects any decision about whether two types are the
same.
• Type application using @ works by substituting a type for a type variable. And
when forall types are nested, substitution is easy to get wrong.
As I present the rules, I take it for granted that the names of quantified type vari‐
ables don’t matter and that substitution is implemented correctly. To enable you to
implement the rules, I then present detailed implementations of type‐equivalence
testing and substitution (Sections 6.6.6 and 6.6.7).
Typing rules for expressions
(EMPTYBEGıN)
∆, Γ ` BEGıN() : unit
A LET expression is well typed if all of the right‐hand sides are well typed, and
if in an environment extended with the types of the bound names, the body is well
typed. The type of the LET expression is the type of the body.
∆, Γ ` ei : τi , 1 ≤ i ≤ n ∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
(LET)
∆, Γ ` LET(hx1 , e1 , . . . , xn , en i, e) : τ
The rule applies equally well to the empty LET.
Recursive letrec bindings in a typed language
6 (letrec [([f1
([f2
: τ1 ] e1 )
: τ2 ] e2 )]
e),
Type systems for
each bound name fi is visible during the evaluation of each right‐hand side ei .
Impcore and µScheme
But at run time, f1 and f2 don’t get their values until after e1 and e2 have been
364 evaluated. The operational semantics leaves their initial values unspecified:
ℓ1 , ℓ2 ∈
/ dom σ (and all distinct)
ρ′ = ρ{f1 7→ ℓ1 , f2 7→ ℓ2 }
σ0 = σ{ℓ1 7→ unspecified, ℓ2 7→ unspecified}
he1 , ρ′ , σ0 i ⇓ hv1 , σ1 i
he2 , ρ′ , σ1 i ⇓ hv2 , σ2 i
he, ρ , σ2 {ℓ1 7→ v1 , ℓ2 7→ v2 }i ⇓ hv, σ ′ i
′
(LETREC2)
hLETREC(hf1 : τ1 , e1 , f2 : τ2 , e2 i, e), ρ, σi ⇓ hv, σ ′ i
While e1 and e2 are being evaluated, the contents of ℓ1 and ℓ2 are unspecified,
and therefore untrustworthy. In particular, the contents of ℓ1 and ℓ2 are in‐
dependent of the types τ1 and τ2 . While e is being evaluated, by contrast, the
contents of ℓ1 and ℓ2 do respect types τ1 and τ2 , and so f1 and f2 can be eval‐
uated safely. To preserve type safety, then, Typed µScheme’s type system must
prevent f1 and f2 from being evaluated until after ℓ1 and ℓ2 have been updated
to hold values v1 and v2 . And in µScheme, the way to keep something from be‐
ing evaluated is to protect it under a LAMBDA. (In a lazy language like Haskell, a
right‐hand side is never evaluated until its value is needed, so Haskell’s letrec is
not restricted in this way.) Typed µScheme uses the same tactic as the ML family
of languages: it requires that the right‐hand sides e1 and e2 be LAMBDA forms.
So letrec is useful only for defining recursive functions, including mutually re‐
cursive functions.
A LETREC is well typed when the corresponding LET is well typed, except that
each right‐hand side ei can refer to any of the bound names xj . So the right‐hand
sides are typechecked in the extended environment Γ{x1 7→ τ1 , . . . , xn 7→ τn }.
Types τ1 to τn , which are written in the syntax, must all have kind ∗.
∆ ` τi :: ∗, 1 ≤ i ≤ n
∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` ei : τi , 1 ≤ i ≤ n
∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
(LETREC)
∆, Γ ` LETREC(hx1 : τ1 , e1 , . . . , xn : τn , en i, e) : τ
As in untyped µScheme, the parser ensures that every ei has the form of a LAMBDA.
This requirement prevents any ei from evaluating an uninitialized xj (see sidebar
above).
A rule for LETſTAR would be annoying to write down directly—it would require
a lot of bookkeeping for environments. Instead, I use syntactic sugar. A LETſTAR is
well typed if the corresponding nest of LETs is well typed.
∆, Γ ` LET(hx1 , e1 i, LETſTAR(hx2 , e2 , . . . , xn , en i, e)) : τ n>0
∆, Γ ` LETſTAR(hx1 , e1 , . . . , xn , en i, e) : τ
(LETſTAR)
∆, Γ ` e : τ
(EMPTYLETſTAR)
∆, Γ ` LETſTAR(hi, e) : τ §6.6.5
A function is well typed if its body is well typed, in an environment that gives Typing rules for
the types of the arguments. These types must be well formed and have kind ∗. The Typed µScheme
type of the body is the result type of the function.
365
∆ ` τi :: ∗, 1 ≤ i ≤ n ∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
(LAMBDA)
∆, Γ ` LAMBDA(hx1 : τ1 , . . . , xn : τn i, e) : τ1 × · · · × τn → τ
An application is well typed if the term being applied has an arrow type, and if
the types and number of actual parameters match the types and number of formal
parameters on the left of the arrow. The type of the application is the type to the
right of the arrow (the result type).
∆, Γ ` e : τ1 × · · · × τn → τ ∆, Γ ` ei : τi , 1 ≤ i ≤ n
(APPLY)
∆, Γ ` APPLY(e, e1 , . . . , en ) : τ
The most interesting rules, which have no counterpart in Typed Impcore,
are for type abstraction and application, which introduce and eliminate polymor‐
phism. The elimination form is simpler. To use (“eliminate”) a polymorphic value,
one chooses the types with which to instantiate the type variables. A type applica‐
tion is well typed if the instantiated term has a quantified type with the expected
number of bound type variables, and if every actual type parameter (τ1 to τn ) has
kind ∗. The notation [α1 7→ τ1 , . . . , αn 7→ τn ] indicates the simultaneous, capture
avoiding substitution of τ1 for α1 , τ2 for α2 , and so on.
∆, Γ ` e : ∀α1 , . . . , αn . τ ∆ ` τi :: ∗, 1 ≤ i ≤ n
(TYAPPLY)
∆, Γ ` TYAPPLY(e, τ1 , . . . , τn ) : τ [α1 7→ τ1 , . . . , αn 7→ τn ]
Simultaneous means “substitute for all the αi ’s at once,” not one at a time. Simul‐
taneous substitution can differ from sequential substitution if some τi contains
an αj . Captureavoiding means that the substitution doesn’t inadvertently change
the meaning of a type variable; the details are explored at length in Section 6.6.7
(page 371).
The TYAPPLY rule justifies my informal claim that the names of quantified type
variables don’t matter. All you can do with a value of quantified type is substitute
for its type variables, and once you substitute for them, they are gone. The names
exist only to mark the correct locations for substitution—no more, and no less.
The introduction form for a quantified type is type abstraction. To create (“in‐
troduce”) a polymorphic value, one abstracts over new type variables using TY‐
LAMBDA. The type variables go into the kind environment ∆. In Typed µScheme,
type variables always stand for types, so they have kind ∗. (In related, more am‐
bitious languages like Haskell or Fω , a type variable may have any kind.) A type
abstraction is well typed if its body is well typed—and the body may refer to the
newly bound type variables.
αi ∈
/ ftv(Γ), 1 ≤ i ≤ n ∆{α1 :: ∗, . . . αn :: ∗}, Γ ` e : τ
(TYLAMBDA)
∆, Γ ` TYLAMBDA(α1 , . . . , αn , e) : ∀α1 , . . . , αn . τ
A type abstraction must also satisfy the side condition αi ∈ / ftv(Γ). Why? The
set ftv(Γ) contains the free type variables of Γ (page 372), and the side condition is
needed to avoid changing the meaning of αi in e. This need is illustrated in Sec‐
tion 6.6.9 (page 377).
Typing rules for definitions
hVAL(it, e), ∆, Γi → Γ′
(EXP)
hEXP(e), ∆, Γi → Γ′
A DEFıNE is syntactic sugar for a suitable VAL‐REC. Indeed, Typed µScheme has
VAL‐REC only because it is easier to typecheck VAL‐REC and LAMBDA independently
than to typecheck DEFıNE directly.
Type checking
The rules above are to be implemented by a type checker, which I hope you will
write (Exercise 19). Type checking requires an expression or definition, a type en‐
vironment, and a kind environment. Calling typeof(e, ∆, Γ) should return a τ
such that Γ ` e : τ , or if no such τ exists, it should raise the exception TypeError.
Calling typdef(d, ∆, Γ) should return a pair (Γ′ , s), where hd, ∆, Γi → Γ′ and
s is a string that represents the type of the thing defined.
366. htype checking for Typed µScheme [[prototype]] 366i≡
typeof : exp * kind env * tyex env ‑> tyex
typdef : def * kind env * tyex env ‑> tyex env * string
To implement these functions, you need function eqType, which tells when two S213b
type tyex 357a
types are equivalent, and function instantiate, which instantiates polymorphic
types. Equivalence and instantiation are the topics of the next two sections.
Many typing rules require that two types be the same. For example, in an if expres‐
sion, the types of the two branches have to be the same. And in Typed µScheme,
two types may be considered the same even if they are not identical. For exam‐
ple, types ∀α . α list → int and ∀β . β list → int are considered to be the
same—the names of bound type variables α and β are irrelevant. The names are ir‐
relevant because the only thing we can do with a quantified type is substitute for its
bound type variables, and once we have substituted, the names are gone. In gen‐
eral, type ∀α1 . τ1 is equivalent to ∀α2 . τ2 if for every possible τ , τ1 [α1 7→ τ ] is
equivalent to τ2 [α2 7→ τ ]. When two types are equivalent, we write τ ≡ τ ′ .
A type‐equivalence relation must preserve type soundness. Soundness says
6 that well‐typed programs don’t go wrong; in particular, if a well‐typed program has
a subterm e of type τ , a type‐soundness theorem allows us to change the program
by substituting any other term e′ of the same type τ . While the changed program
might produce a different answer, it’s still guaranteed not to go wrong. And if there
Type systems for
is an equivalent type, say τ ′ ≡ τ , we must also be allowed to substitute any term
Impcore and µScheme
e′′ of type τ ′ , and the program must still not go wrong.
368
The names of parameters are irrelevant
In Typed µScheme, two types are equivalent if one can be obtained from the other
by renaming bound type variables. And a bound type variable originates as a type
parameter in a type‑lambda. Bound type variables and type‑lambdas may seem
new and mysterious, but just like the parameters in an ordinary lambda, the pa‐
rameters in a type‑lambda can be renamed without changing the meaning of the
code. Let’s look at examples of each.
In (lambda (x) (+ x n)), renaming x to y doesn’t change the meaning of the
code:
(lambda (x) (+ x n)) ; two equivalent μScheme functions
(lambda (y) (+ y n))
And in a (type‑lambda ['a] · · · ), similarly renaming 'a to 'b doesn’t change the
meaning of the code:
368a. htranscript 357bi+≡ ◁ 362 368c ▷
‑> (val id1 (type‑lambda ['a] (lambda ([x : 'a]) x)))
id1 : (forall ['a] ('a ‑> 'a))
‑> (val id2 (type‑lambda ['b] (lambda ([x : 'b]) x)))
id2 : (forall ['b] ('b ‑> 'b))
The renaming gives functions id1 and id2 types that are syntactically different, but
still equivalent: one type is obtained from the other by renaming 'a to 'b. In fact,
function id1 has every type that can be obtained from (forall ['a] ('a ‑> 'a))
by renaming 'a. Here is some evidence:
368b. htypetestsid.tus 368bi≡
(check‑type id1 (forall ['a] ('a ‑> 'a)))
(check‑type id1 (forall ['b] ('b ‑> 'b)))
(check‑type id1 (forall ['c] ('c ‑> 'c)))
When can’t we rename a bound type variable? Imagine a function that takes a
value of any type and returns a value of type 'c; that is, imagine a function of type
(forall ['a] ('a ‑> 'c)). Renaming 'a to 'b doesn’t change the type:
As illustrated above, type (forall ['c] ('c ‑> 'c)) is the type of the identity func‐
tion, and it’s not the same as (forall ['a] ('a ‑> 'c)). Functions of these types
have to behave differently: a function of type (forall ['a] ('a ‑> 'c)) ignores its
argument, and a function of type (forall ['c] ('c ‑> 'c)) returns its argument.
That last renaming is invalid because it captures type variable 'c: 'c is free in
the original type but bound in the new type, so its meaning has been changed.
Whenever we rename a bound type variable, whether it is bound by forall
or type‑lambda, we must not capture any free type variables. (The same re‐
§6.6.6
striction applies to the formal parameters of a lambda expression; for example,
Type equivalence
x can be renamed to y in (lambda (x) (+ x n)), but x can’t be renamed to n;
and typevariable
(lambda (n) (+ n n)) is not the same function!) Also, when we substitute a type τ
renaming
for a free type variable, we must not capture any free type variables of τ .
369
Soundness of type equivalence in Typed µScheme
Why is it sound to consider types equivalent if one can be obtained from the other
by renaming bound type variables? Because if two types differ only in the names
of their bound type variables, no combination of instantiations and substitutions
can distinguish them. To show what it means to distinguish types by instantiation
and substitution, let’s compare the three types above. First I instantiate each type
at τ1 , then I substitute τ2 for free occurrences of 'c:
No matter how τ1 and τ2 are chosen, the first two forall types produce identical
results. But when τ1 and τ2 are chosen intelligently—int and bool will do—the first
two forall types become (int ‑> bool), but the third one becomes (int ‑> int),
which is different.
α≡α µ≡µ
EQUıVFUNſ
τi ≡ τi′ , 1 ≤ i ≤ n τ ≡ τ′
τ1 × · · · × τn → τ ≡ τ1′ × · · · × τn′ → τ ′
EQUıVAPPLıCATıONſ EQUıVQUANTıFıEDſ
τi ≡ τi′ , 1 ≤ i ≤ n τ ≡ τ′ τ ≡ τ′
′
(τ1 , . . . , τn ) τ ≡ (τ1 , . . . , τn′ ) τ ′ ∀α1 , . . . , αn . τ ≡ ∀α1 , . . . , αn . τ ′
These five rules make syntactically identical types equivalent. The next rule makes
two types equivalent if one is obtained from the other by renaming a bound type
variable. Provided new type variable β is not free in τ , any αi can be renamed to β :
β∈
/ ftv(τ ) β∈ / {α1 , . . . , αn } .
∀α1 , . . . , αn . τ ≡ ∀α, . . . , αi−1 , β, αi+1 , α1 , . . . , αn . τ [αi 7→ β]
(EQUıVRENAMED)
The second premise β ∈ / {α1 , . . . , αn } ensures that even after the renaming, the
bound type variables are all distinct.
Like any equivalence relation, type equivalence is symmetric. (Symmetry per‐
mits variables to be renamed on the left side of the ≡ sign, for example.)
6 τ ≡ τ′
(SYMMETRY)
τ′ ≡ τ
Type equivalence is also reflexive and transitive (Exercise 24).
Type systems for Typed µScheme’s type‐equivalence relation is implemented by function eqType.
Impcore and µScheme Given types formed with TYVAR, TYCON, CONAPP, or FUNTY, function eqType imple‐
ments the unique rule that applies to the form.
370
370a. htype equivalence for Typed µScheme 370ai≡ (S405a)
eqType : tyex * tyex ‑> bool
eqTypes : tyex list * tyex list ‑> bool
hinfinite supply of type variables S417ei
fun eqType (TYVAR a, TYVAR a') = a = a'
| eqType (TYCON c, TYCON c') = c = c'
| eqType (CONAPP (tau, taus), CONAPP (tau', taus')) =
eqType (tau, tau') andalso eqTypes (taus, taus')
| eqType (FUNTY (taus, tau), FUNTY (taus', tau')) =
eqType (tau, tau') andalso eqTypes (taus, taus')
| eqType (FORALL (alphas, tau), FORALL (alphas', tau')) =
hBoolean saying if FORALL (alphas, tau) ≡ FORALL (alphas', tau') 370bi
| eqType _ = false
and eqTypes (taus, taus') = ListPair.allEq eqType (taus, taus')
Type variables β1 , . . . , βn (betas) are drawn from an infinite stream. Streams and
stream operators, as well as the particular stream infiniteTyvars, are defined
in the Supplement. Because the stream contains infinitely many type variables,
of which only finitely many can be free in τ or τ ′ , it is guaranteed to hold n good
ones.
Function rename is implemented using substitution; both rename and tysubst
are defined in the next section.
Function eqType can be used in the implementation of any typing rule that re‐
quires two types to be the same. To formalize the use of equivalence instead of
identity, I extend the type system with the following rule, which says that if e has a
type, it also has any equivalent type.
∆, Γ ` e : τ τ ≡ τ′
(EQUıV)
∆, Γ ` e : τ ′
6.6.7 Instantiation and renaming by captureavoiding substitution
§6.6.7
Free and bound type variables
Instantiation and
Type variables may occur free or bound, and we substitute only for free occurrences. renaming by
A free type variable acts like a global variable; a bound type variable acts like a captureavoiding
formal parameter. And a binding occurrence is an appearance next to a forall. All substitution
three kinds of occurrences are shown in this example type: 371
Free occurrence of 'c in A ('c ‑> (forall ['a] ('a ‑> 'c)))
Binding occurrence of 'a in A ('c ‑> (forall ('a) ('a ‑> 'c)))
Bound occurrence of 'a in A ('c ‑> (forall ('a) ('a ‑> 'c)))
Free occurrence of 'c in A ('c ‑> (forall ('a) ('a ‑> 'c)))
As the wording suggests, “free” and “bound” are not absolute properties; they are
relative to a particular type. For example, type variable 'a occurs bound in type
(forall ['a] ('a ‑> 'c)), but it occurs free in type ('a ‑> 'c).
Free type variables can be specified by a proof system. The judgment of the
system is α ∈ ftv(τ ) , which means “α is free in τ .”6 The proof system resembles
the proof system for free term variables in Section 5.6 (page 316):
α ∈ ftv(τi ) α ∈ ftv(τ )
,
α ∈ ftv(α) α ∈ ftv((τ1 , . . . , τn ) τ ) α ∈ ftv((τ1 , . . . , τn ) τ )
CONAPP 357a
α ∈ ftv(τi ) α ∈ ftv(τ )
, diff S217b
α ∈ ftv(τ1 × · · · × τn → τ ) α ∈ ftv(τ1 × · · · × τn → τ ) emptyset S217b
FORALL 357a
357a
α ∈ ftv(τ ) α 6= αi , 1 ≤ i ≤ n FUNTY
. infiniteTyvars
α ∈ ftv(∀α1 , . . . , αn . τ ) S417e
insert S217b
Also, if αi ∈ ftv(τ ), then αi is bound in ∀α1 , . . . , αn . τ . member S217b
type name 303
The free type variables of a type are computed by function freetyvars. Bound rename 375c
type variables are removed using diff. Using foldl, union, diff, and reverse puts reverse S219b
type variables in the set in the order of their first appearance. streamFilter
S231d
371. hsets of free type variables in Typed µScheme 371i≡ (S405a) 372 ▷
streamTake S232d
fun freetyvars t = freetyvars : tyex ‑> name set TYCON 357a
type tyex 357a
let fun free (TYVAR v, ftvs) = insert (v, ftvs)
TYVAR 357a
| free (TYCON _, ftvs) = ftvs
union S217b
| free (CONAPP (ty, tys), ftvs) = foldl free (free (ty, ftvs)) tys
| free (FUNTY (tys, ty), ftvs) = foldl free (free (ty, ftvs)) tys
| free (FORALL (alphas, tau), ftvs) =
union (diff (free (tau, emptyset), alphas), ftvs)
in reverse (free (t, emptyset))
end
6
“Binding occurrence” doesn’t need a proof system; binding occurrences are those introduced by ∀.
The free type variables of a type environment, which are needed to enforce
the side condition in rule TYLAMBDA page 365, are computed by calling function
freetyvarsGamma.
372. hsets of free type variables in Typed µScheme 371i+≡ (S405a) ◁ 371
6 fun freetyvarsGamma Gamma = freetyvarsGamma : tyex env ‑> name set
foldl (fn ((x, tau), ftvs) => union (ftvs, freetyvars tau)) emptyset Gamma
Suppose I wish to substitute 'c for 'b. In example A, there are no occurrences of 'b,
and nothing happens. What about example B? There are no free occurrences of 'b,
just one binding occurrence and one bound occurrence. So also, nothing happens!
And when nothing happens to two equivalent types, the results are still equivalent.
If I mistakenly substitute for the binding occurrence of 'b or for the bound
occurrence of 'b, or for both, the resulting types are no longer be equivalent to ex‐
ample type A.
('c ‑> (forall ['c] ('b ‑> 'c))) ;; WRONG B1
('c ‑> (forall ['b] ('c ‑> 'c))) ;; WRONG B2
('c ‑> (forall ['c] ('c ‑> 'c))) ;; WRONG B3
B1. I substitute for the binding occurrence of 'b but not for the bound occur‐
rence in the body. If I now rename the bound type variable 'c to 'a, I get the
type ('c ‑> (forall ['a] ('b ‑> 'a))). The outer part now matches exam‐
ple A, but the but inner function type ('b ‑> 'a) is not equivalent to exam‐
ple A’s ('a ‑> 'c).
B2. I substitute for the bound occurrence of 'b in the body but not for the bind‐
ing occurrence in the forall. If I now rename the bound type variable 'b
to 'a, I get ('c ‑> (forall ['a] ('c ‑> 'a))), and again, inner function type
('c ‑> 'c) is not equivalent to example A’s ('a ‑> 'c).
B3. I substitute for both binding and bound occurrences of 'b. If I now rename
the bound type variable 'c to 'a, I get ('c ‑> (forall ['a] ('a ‑> 'a))),
and again, inner function type ('a ‑> 'a) is not equivalent to example A’s
('a ‑> 'c).
When I’m substituting for 'b and I hit a forall that binds 'b, I must leave it alone.
If that thought makes you uneasy, imagine “leave it alone” as a three‐step proce‐
dure:
3. Finally rename the bound 'z back to 'b, which again preserves equivalence.
Here’s one more example of not substituting for a bound type variable. I define
a polymorphic value strange of type ∀α . ∀α . α → α: §6.6.7
373a. htranscript 357bi+≡ ◁ 368c 373b ▷ Instantiation and
‑> (val strange renaming by
(type‑lambda ['a] captureavoiding
(type‑lambda ['a] substitution
(lambda ([x : 'a]) x))))
strange : (forall ['a] (forall ['a] ('a ‑> 'a))) 373
To instantiate strange at int, I strip the outer ∀α., and I substitute int for free
occurrences of α in ∀α.α → α. But there are no free occurrences! Type variable α
is bound in ∀α . α → α, and substituting int yields ∀α . α → α:
373b. htranscript 357bi+≡ ◁ 373a
‑> [@ strange int]
<function> : (forall ['a] ('a ‑> 'a))
Substitution must not only avoid substituting for bound occurrences; it must also
avoid changing a free occurrence to a bound occurrence. That is, supposing τ is
substituted for 'c, every type variable that is free in τ must also be free in the result.
As an example, let us substitute (list 'b) for 'c in example types A and B:
('c ‑> (forall ['a] ('a ‑> 'c))) ; example A
('c ‑> (forall ['b] ('b ‑> 'c))) ; example B
((list 'b) ‑> (forall ['a] ('a ‑> (list 'b)))) ; A substituted
((list 'b) ‑> (forall ['b] ('b ‑> (list 'b)))) ; B substituted WRONG
In both examples, we substitute for two free occurrences of 'c. The type we sub‐
stitute has one free occurrence of 'b. In example A, the result has, as expected,
two free occurrences of 'b, But in example B, the second free occurrence of 'b has
become a bound occurrence. We say variable 'b is captured.
Capture is a problem in any computation that involves substitutions—think
macros—and the problem is one we have solved before: it’s the problem of the
faulty let sugar for || in Section 2.13.3 (page 165). The faulty sugar suggests that
(|| e1 e2 ) be implemented by substituting for e1 and e2 in this template:
emptyset S217b
type env 304
(let ([x e1 ]) (if x x e2 )).
freetyvars 371
type name 303
The substitution fails if x appears as a free variable in e2 —when e2 is substituted
type tyex 357a
into the template for ||, variable x is captured and its meaning is changed. Capture union S217b
is avoided by renaming the bound variable x to something that is not free in e2 .
A polymorphic type system avoids capture in the same way: by renaming a
bound type variable. And in the type system, renaming is easy to justify: when we
rename, instead of substituting into example type B, we are substituting into an
equivalent type. In the example above, the only type variable bound in B is 'b, and
we rename it to 'z:
('c ‑> (forall ('z) ('z ‑> 'c))) ; equivalent to B
((list 'b) ‑> (forall ('z) ('z ‑> (list 'b)))) ; and now substituted
Let’s generalize from the example to a specification. To substitute one type τ for
free occurrences of type variable α, without allowing any variable to be captured,
6 the judgment form is τ ′ [α 7→ τ ] ≡ τ ′′ . It is pronounced “τ ′ with α going to τ is
equivalent to τ ′′ .”
Substitution for α changes only α. And it preserves the structure of construc‐
Type systems for tors, constructor applications, and function types.
Impcore and µScheme
α 6= α ′
374 α[α 7→ τ ] ≡ τ α′ [α 7→ τ ] ≡ α′ µ[α 7→ τ ] ≡ µ
τ ′ [α 7→ τ ] ≡ τ ′′
((τ1′ , . . . , τn′ ) τ ′ )[α 7→ τ ] ≡ (τ1′ [α 7→ τ ], . . . , τn′ [α 7→ τ ]) τ ′′
Substitution into a quantified type may substitute for free variables only, and it may
not capture a free type variable of τ :
The second premise prevents variable capture. Substitution can proceed without
capture by substituting into an equivalent type:
τ ′ ≡ τ ′′
.
τ ′ [α 7→ τ ] ≡ τ ′′ [α 7→ τ ]
.
(∀α1 , . . . , αn . τ ′ )[αi 7→ τ ] ≡ (∀α1 , . . . , αn . τ ′ )
In Typed µScheme, substituting for a single type variable isn’t enough; instan‐
tiation substitutes for multiple type variables simultaneously. A substitution is rep‐
resented by an environment of type tyex env, which is passed to function tysubst
as parameter varenv. This environment maps each type variable to the type that
should be substituted for it. If a type variable is not mapped, substitution leaves it
unchanged.
374. hcaptureavoiding substitution for Typed µScheme 374i≡ (S405a) 375c ▷
tysubst : tyex * tyex env ‑> tyex
fun tysubst (tau, varenv) = subst : tyex ‑> tyex
let hdefinition of renameForallAvoiding for Typed µScheme (left as an exercise)i
fun subst (TYVAR a) = (find (a, varenv) handle NotFound _ => TYVAR a)
| subst (TYCON c) = (TYCON c)
| subst (CONAPP (tau, taus)) = CONAPP (subst tau, map subst taus)
| subst (FUNTY (taus, tau)) = FUNTY (map subst taus, subst tau)
| subst (FORALL (alphas, tau)) =
huse varenv to substitute in tau; don’t capture or substitute for any alphas 375bi
in subst tau
end
Substitution into a quantified type must not substitute for a bound variable and
must avoid capturing any variables. Postponing for the moment the issue of cap‐
ture, tysubst prevents substitution for a bound type variable by extending varenv
so that each bound type variable is mapped to itself:
375a. hsubstitute varenv in FORALL (alphas, tau) (OK only if there is no capture) 375ai≡ (375b) §6.6.7
let val varenv' = varenv <+> mkEnv (alphas, map TYVAR alphas)
Instantiation and
in FORALL (alphas, tysubst (tau, varenv'))
renaming by
end
captureavoiding
To avoid capture, tysubst identifies and renames bindings that might capture substitution
a variable. The scenario has three parts:
375
• A type τnew is substituted for a variable that appears free in ∀α1 , . . . , αn . τ .
• Among the free variables of type τnew is one of the very type variables αi that
appears under the ∀.
Below, αi ’s that have to be renamed are put in a set called actual_captures. If the
set is empty, the code above works. Otherwise, the variables in actual_captures
are renamed by function renameForallAvoiding.
375b. huse varenv to substitute in tau; don’t capture or substitute for any alphas 375bi≡ (374)
let val free = freetyvars (FORALL (alphas, tau))
val new_taus = map (subst o TYVAR) free
val potential_captures = foldl union emptyset (map freetyvars new_taus)
val actual_captures = inter (potential_captures, alphas)
in if null actual_captures then
hsubstitute varenv in FORALL (alphas, tau) (OK only if there is no capture) 375ai
else
subst (renameForallAvoiding (alphas, tau, potential_captures))
end
<+> 305f
When capture may occur, function renameForallAvoiding renames the alphas
CONAPP 357a
to avoid potentially captured variables. It must return a type that is equivalent emptyset S217b
to FORALL (alphas, tau) but that does not result in variable capture. In detail, find 305b
renameForallAvoiding([α1 , . . . , αn ], τ, C) returns a type ∀β1 , . . . , βn . τ ′ that has FORALL 357a
freetyvars 371
these properties:
FUNTY 357a
inter S217b
∀β1 , . . . , βn . τ ′ ≡ ∀α1 , . . . , αn . τ , mkEnv 305e
{β1 , . . . , βn } ∩ C = ∅. type name 303
NotFound 305b
renameForall‐
The implementation of renameForallAvoiding is left to you (Exercise 28). Avoiding 397
TYCON 357a
Renaming and instantiation type tyex 357a
TYVAR 357a
union S217b
Renaming is a special case of substitution. It substitutes one set of variables for
another.
375c. hcaptureavoiding substitution for Typed µScheme 374i+≡ (S405a) ◁ 374 376a ▷
rename : name list * name list * tyex ‑> tyex
fun rename (alphas, betas, tau) =
tysubst (tau, mkEnv (alphas, map TYVAR betas))
The Standard ML function List.find takes a predicate and searches a list for an
element satisfying that predicate.
We avoid variable capture because if capture were permitted, the type system could
be subverted: a value of any type could be cast to a value of any other type.
In a world where capture isn’t avoided, subverting the type system requires just
two type‑lambdas and two type variables. We first define a Curried function that
takes an argument, takes a function to apply to the argument, then returns the ap‐
plication. In untyped µScheme, the code might look like this:
376b. hµScheme transcript 376bi≡
‑> (val flip‑apply (lambda (x) (lambda (f) (f x))))
‑> ((flip‑apply '(a b c)) reverse)
(c b a)
‑> (val apply‑to‑symbols (flip‑apply '(a b c)))
‑> (apply‑to‑symbols reverse)
(c b a)
‑> (apply‑to‑symbols cdr)
(b c)
Given flip‑apply, I poke at the hole in the type system: I try to substitute 'a
for 'b and then 'b for 'a. If the first substitution is done incorrectly, 'a is captured,
and I can define a polymorphic function with a senseless type:
376d. hvariablecapture transcript 376ci+≡ ◁ 376c 377a ▷
‑> (type‑lambda ['a] [@ flip‑apply 'a]) ; variable 'a is captured!
<function> : (forall ['a] ('a ‑> (forall ['a] (('a ‑> 'a) ‑> 'a))))
This anonymous function, after it is instantiated and applied, will return a result
of type ∀α . (α → α) → α. That result can be instantiated at any type τ . If I then
supply an identity function of type τ → τ , I get back a value of type τ . Which is
nonsense! A single polymorphic function cannot manufacture a value of an arbi‐
trary type τ , for any τ .
Having captured 'a, I make the nonsense more obvious by instantiating the
problematic result type at 'b:
§6.6.9
377a. hvariablecapture transcript 376ci+≡ ◁ 376d 377b ▷ Preventing capture
‑> (val pre‑cast
with type‑lambda
(type‑lambda ['a 'b]
(lambda ([x : 'a]) 377
[@ ([@ flip‑apply 'a] x) 'b])))
pre‑cast : (forall ['a 'b] ('a ‑> (('b ‑> 'b) ‑> 'b)))
Now you modify pre‑cast by supplying an identity function in the right place (Ex‐
ercise 33 again). Use flip‑apply to define a function cast of type ∀α, β . α → β :
377b. hvariablecapture transcript 376ci+≡ ◁ 377a 377c ▷
‑> (val cast hdefinition of cast (left as an exercise)i)
cast : (forall ['a 'b] ('a ‑> 'b))
Function cast can be used to change a value of any type to any other type. For
example, we can “make a function” out of the number 42. doesn’t work. When we
apply the supposed function, the evaluator reports a bug in the type checker.
377c. hvariablecapture transcript 376ci+≡ ◁ 377b
‑> ([@ cast int (int ‑> int)] 42)
42 : (int ‑> int)
‑> (([@ cast int (int ‑> int)] 42) 0)
bug in type checking: applied non‑function
To make the type system sound, it’s not enough to substitute correctly into quan‐
tified types; we must also take care when introducing them. A quantified type is
BindListLength
introduced by type‑lambda, and to ensure soundness, type‑lambda restricts the 305e
names of its formal (type) parameters: cdr P 162a
type env 304
αi ∈
/ ftv(Γ), 1 ≤ i ≤ n ∆{α1 :: ∗, . . . αn :: ∗}, Γ ` e : τ . eqKind 355b
(TYLAMBDA) 357a
∆, Γ ` TYLAMBDA(α1 , . . . , αn , e) : ∀α1 , . . . , αn . τ FORALL
type kind 355a
kindof 378b
The restriction αi ∈/ ftv(Γ) prevents a form of variable capture. mkEnv 305e
The restriction is necessary because a polymorphic term TYLAMBDA(α, e) can reverse B
be instantiated by substituting any type τ for α. And if α already stands for some‐ type tyex 357a
TYPE 355a
thing else, there’s trouble. As a first example, I can make α already stand for the
TypeError S213c
type of a term variable x; I wrap a type‑lambda around a lambda: typeString S410a
tysubst 374
(type‑lambda ['a] (lambda ([x : 'a]) ...))
Within the ..., the typing environment binds x to 'a, so 'a is a free type variable
of Γ. Suppose an inner type‑lambda were permitted to bind 'a for a second time:
In the position of the ... in the new example, it looks like x and y both have the
same type, but they can be given different types. That puts a hole in the type system.
For example, two values can be compared for equality regardless of their types:
378a. htranscript with no restriction on type‑lambda 378ai≡
‑> (val bad= (type‑lambda ['a] (lambda ([x : 'a])
(type‑lambda ['a] (lambda ([y : 'a])
The functions for equivalence, substitution, and instantiation, which are presented
above, are all key elements of a type checker, which I hope you will write (Exer‐
cise 19). When you do, you can take advantage of some more useful functions,
which are presented below.
A type in the syntax, like the type of a parameter in a lambda abstraction, can’t be
trusted—it has to be checked to make sure it is well formed. In Typed µScheme,
a tyex is well formed if it has a kind (Section 6.6.2, page 355). The kind is com‐
puted by function kindof, which implements the kinding judgment ∆ ` τ :: κ.
This judgment says that given kind environment ∆, type‐level expression τ is well
formed and has kind κ. Given ∆ and τ , kindof(τ , ∆) returns a κ such that
∆ ` τ :: κ, or if no such kind exists, it raises the exception TypeError.
378b. hkind checking for Typed µScheme 378bi≡ (S405a) 380a ▷
fun kindof (tau, Delta) = kindof : tyex * kind env ‑> kind
let hdefinition of internal function kind 378ci kind : tyex ‑> kind
in kind tau
end
The internal function kind computes the kind of tau; the environment Delta is
assumed. Function kind implements the kinding rules in the same way that typeof
implements the typing rules and eval implements the operational semantics.
The kind of a type variable is looked up in the environment.
α ∈ dom ∆
(KıNDINTROVAR)
∆ ` TYVAR(α) :: ∆(α)
Thanks to the parser in Section Q.6, the name of a type variable always begins with
a quote mark, so it is distinct from any type constructor.
378c. hdefinition of internal function kind 378ci≡ (378b) 378d ▷
fun kind (TYVAR a) =
(find (a, Delta)
handle NotFound _ => raise TypeError ("unknown type variable " ^ a))
The kind of a type constructor is also looked up.
µ ∈ dom ∆
(KıNDINTROCON)
∆ ` TYCON(µ) :: ∆(µ)
378d. hdefinition of internal function kind 378ci+≡ (378b) ◁ 378c 379a ▷
| kind (TYCON c) =
(find (c, Delta)
handle NotFound _ => raise TypeError ("unknown type constructor " ^ c))
The kind of a function type is ∗, provided that the argument types and result
type also have kind ∗.
∆ ` τi :: ∗, 1 ≤ i ≤ n ∆ ` τ :: ∗
(KıNDFUNCTıON)
∆ ` τ1 × · · · × τn → τ :: ∗
379a. hdefinition of internal function kind 378ci+≡ (378b) ◁ 378d 379b ▷ §6.6.10
| kind (FUNTY (args, result)) = Other building
let fun badKind tau = not (eqKind (kind tau, TYPE)) blocks of a type
in if badKind result then checker
raise TypeError "function result is not a type"
else if List.exists badKind args then 379
raise TypeError "argument list includes a non‑type"
else
TYPE
end
The argument types are inspected using Standard ML function List.exists, which
corresponds to the µScheme function exists?.
Provided that an applied constructor has an arrow kind, the kind of its appli‐
cation is the arrow’s result kind. The kinds of the argument types must be what is
expected from the arrow’s arguments.
∆ ` τ :: κ1 × · · · × κn ⇒ κ ∆ ` τi :: κi , 1 ≤ i ≤ n
(KıNDAPP)
∆ ` CONAPP(τ, [τ1 , . . . , τn ]) :: κ
A tyex used to describe a variable or parameter must have kind TYPE. Function
asType ensures it.
380
Evaluation in the presence of polymorphism
This chapter is about types, but the code does eventually have to be evaluated.
In Typed µScheme, types have no effect at run time; expressions are therefore eval‐
uated using the same rules as for untyped µScheme. And there are new rules for
evaluating type abstraction and application. These rules specify that the evaluator
behaves as if these type abstraction and application aren’t there.
he, ρ, σi ⇓ hv, σ ′ i
(TYAPPLY)
hTYAPPLY(e, τ1 , . . . , τn ), ρ, σi ⇓ hv, σ ′ i
he, ρ, σi ⇓ hv, σ ′ i
(TYLAMBDA)
hTYLAMBDA(hα1 , . . . , αn i, e), ρ, σi ⇓ hv, σ ′ i
This semantics is related to a program transformation called type erasure: if you
start with a program written in Typed µScheme, and you remove all the TYAPPLYs
and the TYLAMBDAs, and you remove the types from the LAMBDAs and the definitions,
and you rewrite VALREC to VAL, then what’s left is a µScheme program.
The evaluator for Typed µScheme resembles the evaluator for µScheme in
Chapter 5. The code for the new forms acts as if TYAPPLY and TYLAMBDA aren’t there.
380b. halternatives for ev for TYAPPLY and TYLAMBDA 380bi≡ (S411b)
| ev (TYAPPLY (e, _)) = ev e
| ev (TYLAMBDA (_, e)) = ev e
ℓ 6∈ dom σ
he, ρ, σi ⇓ hv, σ ′ i
(VAL)
hVAL(x, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ {ℓ 7→ v}i
ℓ 6∈ dom σ
he, ρ{x 7→ ℓ}, σ{ℓ 7→ unspecified}i ⇓ hv, σ ′ i
(VAL‐REC)
hVAL‐REC(x, τ, e), ρ, σi → hρ{x 7→ ℓ}, σ ′ {ℓ 7→ v}i
These rules are implemented in Appendix Q.
Primitive type constructors of Typed µScheme
The types of the primitive functions have to be written using ML code inside the
interpreter, but the raw representation isn’t easy to write. For example, the type of
cons is represented by this enormous constructed value:
Each of these type constructors creates a type or types that are inhabited by cer‐
tain forms of value. For example, types int and bool are inhabited by values of
the form NUM n and BOOLV b; that’s what eval returns when interpreting an expres‐
sion of type int or bool. What about type unit? That type also needs an inhabitant,
which is defined here:
381b. hutility functions on values (µScheme, Typed µScheme, nanoML) 381bi≡ (S379 S405a)
val unitVal = NIL unitVal : value
This conventional inhabitant is used to represent every value of type unit, which
ensures that when comparing two unit values, the primitive = function always re‐
turns #t.
arithOp S393b
ARROW 355a
Selected primitive functions of Typed µScheme binaryOp S393a
CONAPP 357a
Each primitive function has a name, a value, and a type. Most of them appear in type env 304
ev S411b
the Supplement, but to show you how primitives are defined, a few appear here.
FUNTY 357a
As in Chapter 5, primitive values are made using functions unaryOp, binaryOp, type kind 355a
and arithOp. But if something goes wrong at run time, the Typed µScheme ver‐ kindof 378b
sions don’t raise the RuntimeError exception; they raise BugInTypeChecking. And NIL 361b
TYAPPLY 361a
Typed µScheme’s primitives need types! As the type of the arithmetic primitives,
TYCON 357a
I define arithtype. type tyex 357a
381c. hutility functions and types for making Typed µScheme primitives 381ci≡ (S406d) TYLAMBDA 361a
TYPE 355a
unaryOp : (value ‑> value) ‑> (value list ‑> value)
TypeError S213c
binaryOp : (value * value ‑> value) ‑> (value list ‑> value) typeString S410a
arithOp : (int * int ‑> int) ‑> (value list ‑> value) TYVAR 357a
arithtype : tyex unaryOp S393a
type value 361b
val arithtype =
FUNTY ([inttype, inttype], inttype)
As in Chapter 5, the names, values, and types of the primitives are written in one
long list in chunk hprimitive functions for Typed µScheme :: 381di. That list is used to
build the initial basis.
381d. hprimitive functions for Typed µScheme :: 381di≡ (382c) 382a ▷
("+", arithOp op +, arithtype) ::
("‑", arithOp op ‑, arithtype) ::
("*", arithOp op *, arithtype) ::
("/", arithOp op div, arithtype) ::
The list primitives have polymorphic types.
382a. hprimitive functions for Typed µScheme :: 381di+≡ (382c) ◁ 381d
("null?", unaryOp (BOOLV o (fn (NIL ) => true | _ => false))
, FORALL (["'a"], FUNTY ([listtype tvA], booltype))) ::
The initial basis starts with the kinds of the primitive type constructors, plus the
types and values of the primitive functions and values.
382c. hdefinition of primBasis for Typed µScheme 382ci≡ (S406d)
With the primitives in place, the basis is completed by reading and evaluating the
predefined functions. That code is relegated to the Supplement.
6.7 TYPE ſYſTEMſ Aſ THEY REALLY ARE
elimination form for τ . (And if you are trying to produce a value of type τ ′ , the
body of your function might include the introduction form for τ ′ .) Types also pro‐
vide a relatively painless way of documenting code, and they rule out many silly
programming errors.
If types are good, polymorphic types are better. Polymorphic types help make
code reusable, robustly. The polymorphism in Typed µScheme is easy to imple‐
ment, but unpleasant to use—it should be hidden inside a compiler. But similar
forms of polymorphism can be easy to use, and sometimes a great pleasure. These
are found in Chapters 7 to 9.
A type discipline is usually enforced by a type checker. Most type checkers
are easy to implement because most type systems have one rule for each syntac‐
tic form. But if you add sophisticated features, parts of a type checker can be‐
come more challenging. In Typed µScheme, these parts include type equivalence,
6 which is an interesting aspect of many experimental type systems, and substitu‐
tion, which is a ubiquitous, annoying problem.
FORMATıON RULE A rule that says how to make a well‐formed type. For example,
(array int) is a well‐formed type.
GENERATıVıTY If a language construct always creates a new type distinct from any
other type, that construct is called generative. Examples of generative con‐
structs include C’s struct and ML’s datatype.
KıND A means of classifying TYPE CONſTRUCTORſ and TYPEſ. Using kinds makes
it possible to handle an unbounded number of TYPE CONſTRUCTORſ using
finitely many FORMATıON RULEſ.
MONOTYPE A type that cannot be instantiated. For example, the function type
int list → int is a monotype. (As contrasted with a POLYTYPE.)
PARAMETRıC POLYMORPHıſM The form of POLYMORPHıſM that uses type param‐
eters and ıNſTANTıATıON.
QUANTıFıED TYPE A type formed with the universal quantifier ∀. Also called a
POLYTYPE.
TERM The pointy‐headed theory word for “expression.” More generally, a syntactic
form that is computed with at run time and that may have a TYPE.
§6.8
TYPE A specification for a TERM. Or a means of classifying terms. Or a collection
Summary
of values, called the ıNHABıTANTſ.
385
TYPE ABſTRACTıON In PARAMETRıC POLYMORPHıſM, the ıNTRODUCTıON FORM
for a polymorphic type. In Typed µScheme, written type‑lambda.
TYPE APPLıCATıON In PARAMETRıC POLYMORPHıſM, the ELıMıNATıON FORM for
a polymorphic type. It substitutes actual type parameters for quantified type
variables. It is a form of ıNſTANTıATıON.
TYPE CONſTRUCTOR The fundamental unit from which TYPEſ are built. Type con‐
structors come in various ĸıNDſ. Nullary type constructors such as int and
bool are types all by themselves; they have kind ∗. Other type constructors,
such as list and array, are applied to types to make more types.
TYPE SYſTEM A language’s type system encompasses both the set of TYPEſ that can
be expressed in the language and the rules that say what TERMſ have what
types. A type system may be MONOMORPHıC or POLYMORPHıC.
Pierce (2002) has written a wonderful textbook covering many aspects of typed pro‐
gramming languages. Cardelli (1997) presents an alternative view of type systems;
his tutorial inspired some of the material in this chapter.
Reynolds (1974) presents the polymorphic, typed lambda calculus now known
as System F, which is the basis of Typed µScheme. Reynolds says,
6.9 EXERCıſEſ
The exercises are summarized in Table 6.5. This chapter’s exercises are unusually
diverse; they include programming, adding new typing rules, proving things about
type systems, extending interpreters, and subverting type systems. They include
these favorites:
• Nothing solidifies your understanding of type systems like writing a type
checker. You should write one for Typed µScheme, using the typing rules
as your specification (Exercise 19; use Figures 6.9 to 6.12, which appear
on pages 394 to 396). If you want your type checker to be sound, you will
also want to complete the implementation of capture‐avoiding substitution
(Exercise 28). An easier alternative, or a warmup, would be to extend the
type checker for Typed Impcore so it supports arrays (Exercise 18; use Fig‐
ures 6.6 to 6.8, which appear on pages 391 and 392).
• To understand both the power and the agony of programming with explicit
polymorphism, implement exists? or all? in Typed µScheme (Exercise 9).
2. Introduction forms and elimination forms. In this exercise, you identify syn‐
tactic forms (and their associated rules) as introduction forms or elimina‐
tion forms. The exercise is modeled on communication techniques found in
languages like PML/Pegasus, Concurrent ML, and Haskell.
A expression of type PROTO(τ ) is a set of instructions, or protocol, for com‐
municating with a remote server. Such an expression can be run by a special
syntactic form, which communicates with the server. When a communica‐
tion is run, the local interpreter gets an outcome of type τ . Here is a grammar,
with informal explanations:
exp ::= send exp Send a value to the server
| receive Receive a value from the server
| do x ← exp1 in exp2 Protocol exp1 , whose outcome is x,
followed by exp2
| locally exp Produce result exp locally, without
communicating
| run exp Run a protocol
An exp is given a type by these rules:
PROTO SEND
τ is a type Γ`e:τ
,
PROTO(τ ) is a type Γ ` send e : PROTO(UNıT)
DO
RECEıVE Γ ` e1 : PROTO(τ )
τ is a type Γ, x : τ ` e2 : PROTO(τ ′ ) ,
Γ ` receive : PROTO(τ ) Γ ` do x ← e1 in e2 : PROTO(τ ′ )
LOCALLY RUN
Γ`e:τ Γ ` e : PROTO(τ )
.
Γ ` locally e : PROTO(τ ) Γ ` run e : τ
Classify each rule as a formation rule, an introduction rule, or an elimination
rule. Justify your answers.
3. Counting inhabitants. Type bool is inhabited by the two values #t and #f.
Let’s say type lettergrade is inhabited by values A, B , C , D , and F .
(a) List all the values inhabited by product type bool × lettergrade.
(b) List all the values inhabited by sum type bool + lettergrade.
(c) Are your results consistent with the words “sum” and “product”? Justify
your answer.
6.9.3 Extending a monomorphic language
4. Rules for lists in Typed Impcore. In this exercise, you add lists to Typed Imp‐
core. Use the same technique we use for arrays: devise new abstract syn‐
tax to support lists, and write appropriate type‐formation, type‐introduction,
and type‐elimination rules. The rules should resemble the rules shown in
Section 6.4.
Review the discussion of rules in the sidebar on page 347, and make it obvi‐ §6.9
ous which rules are formation rules, which rules are introduction rules, and Exercises
which rules are elimination rules: Divide your rules into three groups and
label each group. 389
• Some rules can be classified just by looking to see where list types
appear. For example, a rule for null? should have a list type in the
premise but not in the conclusion, so null? has to be an elimination
form. Similarly, a car rule should have a list type in the premise but not
necessarily in the conclusion, so it too has to be an elimination form.
• Other rules have list types in both premises and conclusion. When you
have forms like cons and cdr, which both take and produce lists, you
have to fall back on thinking about information. Does a form put new
information into a value, which can later be extracted by another form?
Then it is an introduction form. Does a form put in no new informa‐
tion, but only extract information that is already present? Then it is an
elimination form.
Your abstract syntax should cover all the list primitives defined in Chapter 2:
the empty list, test to see if a list is empty, cons, car, and cdr. Your abstract
syntax may differ from the abstract syntax used in µScheme.
Be sure your rules are deterministic: it should be possible to compute the
type of an expression given only the syntax of the expression and the current
type environment.
5. Rules for records in Typed Impcore. Following the same directions as in Exer‐
cise 4, give typing rules for records with named fields.
6. Rules for sums in Typed Impcore. Following the same directions as in Exer‐
cise 4, give typing rules for sums with named variants.
7. Rules for mutable references in Typed Impcore. Mutable cells can be represented
by a type constructor ref. The appropriate operations are ref, !, and :=.
The function ref is like the function allocate in Chapter 2; applying ref to
a value v allocates a new mutable cell and initializes it to hold v . Applying ! to
a mutable cell returns the value contained in that cell. Applying := to a mu‐
table cell and a value replaces the contents of the cell with the value. (These
functions are also part of Standard ML.)
Give typing rules for a type constructor for mutable cells. (See also Exer‐
cise 13.)
(a) Both functions should have the same polymorphic type. Give it.
(b) Write an implementation of each function.
9. Higherorder, polymorphic linear search. Implement exists? and all? in
Typed µScheme.
(a) Both functions should have the same polymorphic type. Give it.
(a) What is the kind of the type constructor queue? Add it to the initial ∆.
(b) What are the types of empty‑queue, empty?, get‑first, get‑rest,
and put?
(c) Add the new primitive functions to the initial Γ and ρ. You will need to
write implementations in Standard ML.
11. Add pairs to Typed µScheme. Without changing the abstract syntax, values,
type checker, or evaluator of Typed µScheme, extend Typed µScheme with
the pair type constructor and the polymorphic functions pair, fst, and snd.
(a) What is the kind of the type constructor pair? Add it to the initial ∆.
(b) What are the types of pair, fst, and snd?
(c) Add the new primitive functions to the initial Γ and ρ. As you add them
to ρ, you can use the same implementations that we use for cons, car,
and cdr.
12. Add sums to Typed µScheme. Without changing the abstract syntax, values,
type checker, or evaluator of Typed µScheme, extend Typed µScheme with
the sum type constructor and the polymorphic functions left, right, and
either.
(a) What is the kind of the type constructor sum? Add it to the initial ∆.
(b) What are the types of left, right, and either?
(c) Page 349 gives algebraic laws for pair primitives in a monomorphic lan‐
guage. If the sum primitives were added to a monomorphic language,
what would be the laws relating LEFT, RıGHT, and EıTHER?
(d) Since left, right, and either have the polymorphic types in part 12(b),
what are the laws relating them?
(e) Add left, right, and either to the initial Γ and ρ of Typed µScheme.
Try representing a value of sum type as a PAIR containing a tag and a
value.
UNıTTYPE INTTYPE BOOLTYPE
τ is a type
UNıT is a type ıNT is a type BOOL is a type
ARRAYTYPE §6.9
τ is a type
Exercises
ARRAY(τ ) is a type
391
LıTERAL
Γξ , Γϕ , Γρ ` e : τ
Γξ , Γϕ , Γρ ` LıTERAL(v) : ıNT
FORMALVAR GLOBALVAR
x ∈ dom Γρ x∈/ dom Γρ x ∈ dom Γξ
Γξ , Γϕ , Γρ ` VAR(x) : Γρ (x) Γξ , Γϕ , Γρ ` VAR(x) : Γξ (x)
FORMALAſſıGN GLOBALAſſıGN
x ∈ dom Γρ Γρ (x) = τ x∈
/ dom Γρ x ∈ dom Γξ Γξ (x) = τ
Γξ , Γϕ , Γρ ` e : τ Γξ , Γϕ , Γρ ` e : τ
Γξ , Γϕ , Γρ ` ſET(x, e) : τ Γξ , Γϕ , Γρ ` ſET(x, e) : τ
IF
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e 2 : τ Γξ , Γϕ , Γρ ` e3 : τ
Γξ , Γϕ , Γρ ` ıF(e1 , e2 , e3 ) : τ
WHıLE
Γξ , Γϕ , Γρ ` e1 : BOOL Γξ , Γϕ , Γρ ` e2 : τ
Γξ , Γϕ , Γρ ` WHıLE(e1 , e2 ) : UNıT
BEGıN
Γξ , Γϕ , Γρ ` e1 : τ1 ··· Γξ , Γϕ , Γρ ` en : τn
Γξ , Γϕ , Γρ ` BEGıN(e1 , . . . , en ) : τn
EMPTYBEGıN
Γξ , Γϕ , Γρ ` BEGıN() : UNıT
APPLY
Γϕ (f ) = τ1 × · · · × τn → τ Γξ , Γϕ , Γρ ` ei : τi , 1≤i≤n
Γξ , Γϕ , Γρ ` APPLY(f, e1 , . . . , en ) : τ
EQ PRıNTLN
Γξ , Γϕ , Γρ ` e 1 : τ Γξ , Γϕ , Γρ ` e2 : τ Γξ , Γϕ , Γρ ` e : τ
Γξ , Γϕ , Γρ ` EQ(e1 , e2 ) : BOOL Γξ , Γϕ , Γρ ` PRıNTLN(e) : UNıT
6 OLDVAL
Γξ , Γϕ , {} ` e : τ Γξ (x) = τ
EXP
Γξ , Γϕ , {} ` e : τ
hVAL(x, e), Γξ , Γϕ i → hΓξ , Γϕ i hEXP(e), Γξ , Γϕ i → hΓξ , Γϕ i
Type systems for
Impcore and µScheme DEFıNE
τ1 , . . . , τn are types
392 τf = τ1 × · · · × τn → τ
f∈ / dom Γϕ
Γξ , Γϕ {f 7→ τf }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ {f 7→ τf }i
REDEFıNE
τ1 , . . . , τn are types
Γϕ (f ) = τ1 × · · · × τn → τ
Γξ , Γϕ {f 7→ τ1 × · · · × τn → τ }, {x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
hDEFıNE(f, (hx1 : τ1 , . . . , xn : τn i, e : τ )), Γξ , Γϕ i → hΓξ , Γϕ i
13. Add references to Typed µScheme. In Typed µScheme, it is not necessary to add
any special abstract syntax to support mutable cells as in Exercise 7. Give the
kind of the ref constructor and the types of the operations ref, !, and :=.
14. Add simple polymorphic records to Typed µScheme. Extend Typed µScheme by
adding polymorphic records with named fields. Types of fields are not speci‐
fied; instead, the extension creates functions that work like the record func‐
tions in Chapter 2, except these functions are polymorphic.
(a) Add a new form of definition. It should have this concrete syntax:
def ::= (record recordname ( fieldname ))
The abstract syntax can be this:
RECORD of name * name list
(a) Add a new kind of definition. It should have this concrete syntax:
def ::= (typed‑record
(recordname 'typevariablename )
( [fieldname : type] ))
This sort of record is also polymorphic, but under more control: type
parameters are listed explicitly, and the type of each field is declared.
The abstract syntax can be this:
TYPED_RECORD of name * name list * (name * tyex) list
Here is an example.
393b. hexercise transcript 393ai+≡ ◁ 393a
‑> (typed‑record (assoc 'a) ([key : sym] [value : 'a]))
‑> (val p ((@ make‑assoc int) 'class 152))
p : (assoc int)
‑> ((@ assoc‑key int) p)
class : sym
‑> ((@ assoc‑value int) p)
152 : int
KıNDFORMATıONARROW
KıNDFORMATıONTYPE
κ1 , . . . , κn are kinds κ is a kind
κ is a kind
∗ is a kind κ1 × · · · × κn ⇒ κ is a kind
6 Figure 6.9: Kind‐formation rules for Typed µScheme
(b) Using the typing rules from the chapter, give a derivation tree proving
the correctness of your answer to part (a).
17. The type of a polymorphic function in extended Typed µScheme. Suppose we get
tired of writing @ signs everywhere, so we extend Typed µScheme by making
PAıR, FſT, and ſND abstract syntax instead of functions.
(b) Using the typing rules from the chapter, give a derivation tree proving
the correctness of your answer to part (a).
18. Type checking for arrays. Finish the type checker for Typed Impcore so that it
handles arrays. It is sufficient to implement the four cases in code chunk 348.
19. Type checking for Typed µScheme. Write a type checker for Typed µScheme.
That is, implement typdef in code chunk 366. Although you could write this
checker by cloning and modifying the type checker for Typed Impcore, you
VAR
x ∈ dom Γ Γ(x) = τ
∆, Γ ` e : τ
∆, Γ ` VAR(x) : τ
SET WHıLE
∆, Γ ` e : τ x ∈ dom Γ Γ(x) = τ ∆, Γ ` e1 : bool ∆, Γ ` e2 : τ
§6.9
∆, Γ ` ſET(x, e) : τ ∆, Γ ` WHıLE(e1 , e2 ) : unit Exercises
IF 395
∆, Γ ` e1 : bool ∆, Γ ` e2 : τ ∆, Γ ` e3 : τ
∆, Γ ` ıF(e1 , e2 , e3 ) : τ
BEGıN
EMPTYBEGıN
∆, Γ ` ei : τi , 1 ≤ i ≤ n
∆, Γ ` BEGıN(e1 , . . . , en ) : τn ∆, Γ ` BEGıN() : unit
LET
∆, Γ ` ei : τi , 1 ≤ i ≤ n ∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
∆, Γ ` LET(hx1 , e1 , . . . , xn , en i, e) : τ
LETſTAR
∆, Γ ` LET(hx1 , e1 i, LETſTAR(hx2 , e2 , . . . , xn , en i, e)) : τ n>0
∆, Γ ` LETſTAR(hx1 , e1 , . . . , xn , en i, e) : τ
EMPTYLETſTAR
∆, Γ ` e : τ
∆, Γ ` LETſTAR(hi, e) : τ
LETREC
∆ ` τi :: ∗, 1 ≤ i ≤ n
∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` ei : τi , 1 ≤ i ≤ n
∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
∆, Γ ` LETREC(hx1 : τ1 , e1 , . . . , xn : τn , en i, e) : τ
LAMBDA
∆ ` τi :: ∗, 1 ≤ i ≤ n ∆, Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
∆, Γ ` LAMBDA(hx1 : τ1 , . . . , xn : τn i, e) : τ1 × · · · × τn → τ
APPLY
∆, Γ ` e : τ1 × · · · × τn → τ ∆, Γ ` ei : τi , 1 ≤ i ≤ n
∆, Γ ` APPLY(e, e1 , . . . , en ) : τ
TYLAMBDA
αi ∈
/ ftv(Γ), 1 ≤ i ≤ n ∆{α1 :: ∗, . . . αn :: ∗}, Γ ` e : τ
∆, Γ ` TYLAMBDA(α1 , . . . , αn , e) : ∀α1 , . . . , αn . τ
TYAPPLY
∆, Γ ` e : ∀α1 , . . . , αn . τ ∆ ` τi :: ∗, 1 ≤ i ≤ n
∆, Γ ` TYAPPLY(e, τ1 , . . . , τn ) : τ [α1 7→ τ1 , . . . , αn 7→ τn ]
6 VALREC
∆ ` τ :: ∗
∆, Γ{x 7→ τ } ` e : τ EXP
Type systems for e has the form LAMBDA(· · ·) hVAL(it, e), ∆, Γi → Γ′
Impcore and µScheme hVAL‐REC(x, τ, e), ∆, Γi → Γ{x 7→ τ } hEXP(e), ∆, Γi → Γ′
396 DEFıNE
hVAL‐REC(f, τ1 × · · · × τn → τ, LAMBDA(hx1 : τ1 , . . . , xn : τn i, e)), ∆, Γi → Γ′
hDEFıNE(f, τ, hx1 : τ1 , . . . , xn : τn i, e), ∆, Γi → Γ′
will get better results if you build a checker from scratch by following the typ‐
ing rules for Typed µScheme, which are shown in Figure 6.11 on the previous
page and in Figure 6.12 above.
6.9.8 Metatheory
20. Types are unique. Prove that an expression in Typed Impcore has at most one
type. That is, prove that given environments Γξ , Γϕ , Γρ and abstract‐syntax
tree e, there is at most one τ such that Γξ , Γϕ , Γρ ` e : τ .
21. Desugaring preserves types. The sidebar on page 362 notes that the only defi‐
nition form we really need is val; define and val‑rec can be expressed as
syntactic sugar. The desugaring of define is given in the text; in this exer‐
cise, you desugar val‑rec.
(a) Express VAL‐REC as syntactic sugar. That is, specify a translation from
an arbitrary VAL‐REC form into a combination of VAL and LETREC
forms.
(b) Prove that your translation preserves typing. That is, prove that a VAL‐
REC form is well typed if and only if its desugaring is well typed. And
prove that when both are well typed, the final type environments on the
right‐hand side of the → judgment are equal.
22. The type of a Typed Impcore expression is well formed. Using a metatheoretic
argument about typing derivations in Typed Impcore, prove that if there is a
derivation of a typing judgment Γξ , Γϕ , Γρ ` e : τ , there is also a derivation
of the judgment “τ is a type.” Use structural induction on the derivation of
the typing judgment.
23. The type of a Typed µScheme expression is well formed. In Typed µScheme, types
like list and pair are well formed, with kinds ∗ ⇒ ∗ and ∗ × ∗ → ∗, respec‐
tively, but they are not the types of any term: no expression can have type
list or pair. The type of a term must have kind ∗. Using a metatheoretic
argument about typing derivations in Typed µScheme, prove that if there is
a derivation of a typing judgment ∆, Γ ` e : τ , there is also a derivation of §6.9
the judgment ∆ ` τ :: ∗. Use structural induction on the derivation of the Exercises
typing judgment. 397
24. “Type equivalence” is an equivalence. Prove that ≡, as defined in Section 6.6.6
(page 367), is an equivalence relation:
In each case, structure your proof by assuming you have a derivation of the
fact or facts assumed, and construct a derivation of the conclusion.
25. Type checking terminates. Using an argument about the rules in the type sys‐
tem, prove that type checking for Typed Impcore always terminates.
26. Type checking is sound. Show that if an expression in Typed Impcore has a
type, and if the values stored in the value environments ξ , ϕ, and ρ inhabit
the types in the corresponding type environments, then the eval function
never raises the exception BugInTypeChecking.
27. Proof that substitution terminates. Function tysubst on page 374 works by
defining and calling an inner recursive function subst, with which tysubst
is mutually recursive. We need to know that no matter what Typed µScheme
code a programmer writes, tysubst and subst terminate. Of particular con‐
cern is the recursive call in chunk 375b: given a FORALL type, the code makes
a recursive call on a similar FORALL type. Could this process repeat forever?
Prove that tysubst terminates by showing that at every recursive call some‐
thing is getting smaller. You might consider assigning each type a pair of LeftAsExercise
numbers and show that the pair shrinks lexicographically. One number S213b
type name 303
worth considering is the number of bound type variables that are in the range
type tyex 357a
of the substitution.
28. Renaming type variables. In substitution, rename type variables to avoid cap‐
ture: Given a type ∀α1 , . . . , αn . τ and a set C of captured type variables,
rename as many αi ’s as necessary to avoid conflicts with variables in C and
with free variables of τ . Do so in the body of function renameForallAvoiding,
which is nested within function tysubst on page 374:
397. hdefinition of renameForallAvoiding for Typed µScheme [[prototype]] 397i≡
renameForallAvoiding : name list * tyex * name set ‑> tyex
fun renameForallAvoiding (alphas, tau, captured) =
raise LeftAsExercise "renameForallAvoiding"
Calling renameForallAvoiding([α1 , . . . , αn ], τ, C) must choose variables βi
not in C and return a type ∀β1 , . . . , βn . τ ′ with these properties:
∀β1 , . . . , βn . τ ′ ≡ ∀α1 , . . . , αn . τ ,
6 {β1 , . . . , βn } ∩ C = ∅.
fun freshName (alpha, avoid) = freshName : name * name set ‑> name
29. Changing a thing’s type can break the type system. If a global variable or function
is already defined, Typed Impcore doesn’t let you write a new definition at a
different type. To show why such definitions aren’t permitted, complete this
exercise:
(a) Remove the restriction that a val binding may not change the type of
the value bound. (An easy way to do this is to change the condition in
chunk 341c so that it says “if true.”)
(b) With the restriction removed, create a Typed Impcore program whose
evaluation raises BugInTypeChecking, e.g., by adding 1 to an array.
(c) Restore the restriction on val, and remove the restriction that a define
binding may not change the type of a function.
(d) With the restriction removed, create a Typed Impcore program whose
evaluation raises BugInTypeChecking, e.g., by doing an array lookup
on an integer.
30. Polymorphic mutable reference cells are unsound. In a polymorphic system, the
ref constructor (Exercise 13) leads to unsoundness: it can be used to subvert
the type system. You can wrap ref in a type‑lambda in a way that allows
you instantiate a polymorphic, mutable data structure at any type you want.
You can then instantiate it at one type with := and at another type with !,
enabling you to write a function that, for example, converts a Boolean to an
integer.
31. A type‑lambda may not abstract over a variable in the environment. In Typed
µScheme, remove the restriction that a type‑lambda may not abstract over §6.9
a type variable that’s free in the type environment. Now Exercises
32. Misuse of type‑lambda can give any term any type. The discussion of Typed
µScheme rule TYLAMBDA on page 365 observes that if the side conditions
are not enforced, then
{α :: ∗}, {x : α} ` TYLAMBDA(α, x) : ∀α . α.
33. Variable capture can break the type system. In Typed µScheme, change the code InternalError
S219e
in chunk 375b so that substitution is always done naïvely, in a way that al‐
intString S214c
lows the capture of a ∀‐bound variable. (It suffices to write if true instead member S217b
of if null actual_captures.) type name 303
naturals S230c
(a) Define a typed, polymorphic version of function flip‑apply from streamFilter
Typed Impcore and Typed µScheme represent two extremes. Typed Impcore is
easy to program in and easy to write a type checker for, but because it is monomor‐
phic, it cannot accept polymorphic functions, and it can accommodate new type
constructors and polymorphic operations only if its syntax and type checker are
extended. Typed µScheme is also easy to write a type checker for, and as a poly‐
morphic language, it can accept polymorphic functions, and it can accommodate
new type constructors and polymorphic functions with no change to its syntax or
its type checker. But Typed µScheme is difficult to program in: as Milner observed,
supplying a type parameter at every use of every polymorphic value soon becomes
intolerable. To combine the expressive power of polymorphism with great ease of
programming, this chapter presents a third point in the design space: nano‐ML.
Nano‐ML is expressive, easy to extend, and also easy to program in. This ease of
use is delivered by a new typing algorithm: instead of type checking, nano‐ML uses
type inference.
A language with type inference doesn’t require explicit type annotations; the
types of variables and parameters are discovered by an algorithm. Type inference
is used in such languages as Haskell, Miranda, OCaml, Standard ML, and Type‐
Script. Type inference works with a limited form of polymorphism: typically the
HindleyMilner type system. In this type system, a quantified ∀ type may appear only
at top level; a ∀ type may not be passed to a type constructor. In particular, a ∀ type
may not appear as an argument in a function type. This restriction makes type
inference decidable.
In this chapter, the Hindley‐Milner type system and its type inference are illus‐
trated by nano‐ML, a language that is closely related to Typed µScheme.
• Like Typed µScheme, nano‐ML has polymorphic types that are checked at
compile time.
401
• Unlike Typed µScheme, nano‐ML has implicit types. In nano‐ML, the pro‐
grammer never writes a type or a type constructor.
• Unlike Typed µScheme, nano‐ML has no mutation. Nano‐ML lacks set, and
7 its names stand for values, not for mutable locations. Because there is no
mutation, nano‐ML programs are nearly always written in applicative style.
Imperative actions are limited to printing and error primitives.
ML and type • Unlike Typed µScheme, nano‐ML restricts polymorphism: quantified types
inference may appear only at top level. This restriction enables nano‐ML to instantiate
polymorphic values automatically and also to introduce polymorphic types
402 automatically. Explicit @ and type‑lambda are not needed.
Nano‐ML, Typed µScheme, and µScheme are closely related. If the types are
erased from a Typed µScheme program, the result is a valid µScheme program.
And if the program does not use set or while, and if it uses type‑lambda appropri‐
ately, it is also a valid nano‐ML program.
Like the interpreter for Typed µScheme, nano‐ML’s interpreter is based on the
µScheme interpreter from Chapter 5. And as with the type checker from Chapter 6,
substantial parts of the implementation are left as exercises.
Aside from its type system, nano‐ML differs from µScheme by forbidding mu‐
tation.1 Mutation is the archetypal example of an imperative feature. Although
nano‐ML does not have mutation, it does have other imperative features: printing
primitives, error, and begin (see sidebar).
Nano‐ML and µScheme have subtly different definition forms. In µScheme, a
val definition can mutate an existing binding, but in nano‐ML, val always creates
a new binding. To define a recursive function, nano‐ML uses a val‑rec defini‐
tion form like Typed µScheme’s val‑rec. And like Typed µScheme, nano‐ML uses
define as syntactic sugar for a combination of val‑rec and lambda.
Nano‐ML needs fewer primitives than µScheme. Because nano‐ML has a type
system, every symbol, number, Boolean, and function is identified as such at com‐
pile time—so nano‐ML doesn’t need type predicates symbol?, number?, boolean?,
or function?. Nano‐ML does need the null? predicate, which is used to tell the
difference between empty and nonempty lists, but it does not also need pair?.
Except for the addition of val‑rec, the concrete syntax of nano‐ML, which
is shown in Figure 7.1 on page 404, is mostly a subset of that of µScheme. But
nano‐ML’s syntax also includes forms for type‐related unit tests: check‑type,
check‑type‑error, and check‑principal‑type.
The check‑type test serves the same role as the corresponding test in Typed
µScheme, but as explained in Section 7.4.6, it is more permissive. To test for type
equivalence in nano‐ML, use check‑principal‑type.
402. htranscript 402i≡ 409b ▷
‑> (check‑principal‑type revapp
(forall ['a] ((list 'a) (list 'a) ‑> (list 'a))))
‑> (define revapp (xs ys)
(if (null? xs)
ys
(revapp (cdr xs) (cons (car xs) ys))))
Despite the fact the Impcore is a procedural language and the dialects of
µScheme are functional languages, all three share syntactic forms devoted to
imperative features: set, while, and begin. These forms are used more heavily
§7.1
in procedural languages; functional languages emphasize let binding, function
NanoML: A
application, and recursion. In nano‐ML, imperative features are so little empha‐
nearly applicative
sized that set and while are entirely absent, and begin is used rarely—primarily
language
for printf‐style debugging.
What are imperative features, and why do we care? A feature is imperative if 403
when the feature is used, different orders of evaluation can produce different results.a
• The set form, also called assignment or mutation, is an imperative fea‐
ture: if two expressions assign to the same mutable location, the order of
evaluation matters. In particular, after two assignments, the second as‐
signment determines what value the location holds.
• Input and output are imperative features. For example, if different print
expressions are evaluated in different orders, the program’s output is dif‐
ferent. Similarly, if a program reads x and y from its input, it may produce
different results depending on the order in which the variables are read.
• Exceptions are an imperative feature: if different expressions raise dif‐
ferent exceptions, order matters. Which exception is raised depends on
which expression is evaluated first. In µScheme and nano‐ML, error is
a similar imperative feature, because the error message depends on the
order of evaluation.
numeral ::= token composed only of digits, possibly prefixed with a plus
or minus sign
*name ::= token that is not a bracket, a numeral, or one of the “re‐
served” words shown in typewriter font
Nano‐ML’s abstract syntax is the same as µScheme’s, minus WHILEX and SET.
404. hdefinitions of exp and value for nanoML 404i≡ (S419b)
datatype exp = LITERAL of value
| VAR of name
| IFX of exp * exp * exp
| BEGIN of exp list
| APPLY of exp * exp list
| LETX of let_flavor * (name * exp) list * exp
| LAMBDA of name list * exp
and let_flavor = LET | LETREC | LETSTAR
and hdefinition of value for nanoML 405bi
The BEGIN form is intended for use with primitive functions println and print.
Except for VALREC, definitions are as in µScheme.
405a. hdefinition of def for nanoML 405ai≡ (S419b)
datatype def = VAL of name * exp
| VALREC of name * exp
| EXP of exp
| DEFINE of name * (name list * exp)
In the operational semantics, nano‐ML and µScheme have the same values, §7.3
and their representations are similar enough that I can reuse the projection, em‐ Operational
bedding, and printing functions from Chapter 5. semantics
405b. hdefinition of value for nanoML 405bi≡ (404)
value = SYM of name 405
| NUM of int
| BOOLV of bool
| NIL
| PAIR of value * value
| CLOSURE of lambda * value env ref
| PRIMITIVE of primop
withtype primop = value list ‑> value (* raises RuntimeError *)
and lambda = name list * exp
Because nano‐ML doesn’t have mutation and because the effects of its imperative
primitives aren’t specified formally, its operational semantics is simple. Its abstract
machine has no locations and no store; evaluating an expression just produces a
value. The judgment is he, ρi ⇓ v . The environment ρ maps a name to a value, not
to a mutable location as in µScheme. And evaluating a definition produces a new
environment; the form of that judgment is hd, ρi → ρ′ .
(LıTERAL)
hLıTERAL(v), ρi ⇓ v
x ∈ dom ρ
(VAR)
hVAR(x), ρi ⇓ ρ(x)
he1 , ρi ⇓ v1 v1 6= BOOLV(#f) he2 , ρi ⇓ v2
(IFTRUE)
hıF(e1 , e2 , e3 ), ρi ⇓ v2
he1 , ρi ⇓ v1 v1 = BOOLV(#f) he3 , ρi ⇓ v3
(IFFALſE)
hıF(e1 , e2 , e3 ), ρi ⇓ v3
The rules for BEGıN are a cheat; the purpose of BEGıN is to force order of evalua‐
tion, but these rules are so simplified that they don’t enforce an order of evaluation.
(EMPTYBEGıN)
hBEGıN(), ρi ⇓ NıL
7 he1 , ρi ⇓ v1 he2 , ρi ⇓ v2 ··· hen , ρi ⇓ vn
(BEGıN)
hBEGıN(e1 , e2 , . . . , en ), ρi ⇓ vn
Just as in µScheme, LAMBDA captures an environment in a closure, and APPLY
ML and type
uses the captured environment. Because nano‐ML does not store actual parame‐
inference
ters in mutable locations, its rules are simpler than µScheme’s rules.
406 (MĸCLOſURE)
hLAMBDA(hx1 , . . . , xn i, e), ρi ⇓ (|LAMBDA(hx1 , . . . , xn i, e), ρ |)
he, ρi ⇓ (|LAMBDA(hx1 , . . . , xn i, ec ), ρc |)
hei , ρi ⇓ vi , 1 ≤ i ≤ n
hec , ρc {x1 7→ v1 , . . . , xn 7→ vn }i ⇓ v
(APPLYCLOſURE)
hAPPLY(e, e1 , . . . , en ), ρi ⇓ v
The semantic rule for applying a nano‐ML primitive is to apply the function
attached to that primitive. The implementation is equally simple.
he, ρi ⇓ PRıMıTıVE(f )
hei , ρi ⇓ vi , 1 ≤ i ≤ n
f (v1 , . . . , vn ) = v
(APPLYPRıMıTıVE)
hAPPLY(e, e1 , . . . , en ), ρi ⇓ v
Because a LET‐bound name stands for a value, not a location, rules for LET
forms are also simplified.
hei , ρi ⇓ vi , 1 ≤ i ≤ n
he, ρ{x1 7→ v1 , . . . , xn 7→ vn }i ⇓ v
(LET)
hLET(hx1 , e1 , . . . , xn , en i, e), ρi ⇓ v
As in µScheme, a LETſTAR expression requires a sequence of environments.
he1 , ρ0 i ⇓ v1 ρ1 = ρ0 {x1 7→ v1 }
..
.
he, ρn−1 i ⇓ vn ρn = ρn−1 {xn 7→ vn }
he, ρn i ⇓ v
(LETſTAR)
hLETſTAR(hx1 , e1 , . . . , xn , en i, e), ρ0 i ⇓ v
LETREC is the tricky one. The expressions are evaluated in an environment ρ′
in which their names are already bound to the resulting values. In other words,
to evaluate each ei , we have to have ρ′ , but to build ρ′ , we have to know all the vi ’s.
It seems like it should be impossible to make progress, but because the expressions
are all lambda abstractions, we can pull it off.
ρ′ = ρ{x1 → 7 v1 , . . . , xn 7→ vn }
′
he1 , ρ i ⇓ v1 ··· hen , ρ′ i ⇓ vn
′
he, ρ i ⇓ v
(LETREC)
hLETREC(hx1 , e1 , . . . , xn , en i, e), ρi ⇓ v
Because each ei is a LAMBDA, evaluating it is going to produce a closure that cap‐
tures ρ′ and the body of the LAMBDA. And in eval, that makes it possible to build ρ′
without calling eval recursively (chunk S430a). The resulting ρ′ satisfies the equa‐
tions in the premises, and the implementation closes the loop by stuffing ρ′ into
the mutable cell contained in each closure.
7.3.2 Rules for evaluating definitions
he, ρi ⇓ v
(VAL)
hVAL(x, e), ρi → ρ{x 7→ v}
• Like Typed µScheme but unlike µScheme, nano‐ML has VAL‐REC. The se‐
mantics requires a ρ′ that binds f to a closure containing ρ′ .
hVAL(it, e), ρi → ρ′
(EXP)
hEXP(e), ρi → ρ′
Like other type systems, the type system of nano‐ML determines which terms have
types, which in turn determines what definitions are accepted by the interpreter.
As before, the types of terms are specified by a formal proof system. The system
uses the same elements as the type system of Typed µScheme.
7 • A type built with type variables, type constructors, and constructor applica‐
tion is written using the metavariable τ .
τ ::= α µ (τ1 , . . . , τn ) τ
ML and type
A τ is called a type.
inference
• A quantified type is written using the metavariable σ .
408
σ ::= ∀α1 , . . . , αn . τ
But in the text, the type constructor goes after its arguments, as in int list. This
is the way types are written in ML source code.
In nano‐ML, all type constructors are predefined; no program can add new
ones. New type constructors can be added in the more advanced bridge languages
µML (Chapter 8) and Molecule (Chapter 9). In nano‐ML, the predefined construc‐
tors arguments and function appear in the typing rules for functions. Other con‐
structors, like bool, int, sym, and so on, give types to literals or primitives.
To write types made with arguments and function, I use ML’s abbreviations.
Type Abbreviation
(τ1 , τ2 ) function τ1 → τ2
(τ1 , . . . , τn ) arguments τ1 × τ2 · · · × τn
In nano‐ML, as in Typed µScheme, a type environment is written using the
Greek letter Γ. In nano‐ML, a type environment Γ maps a term variable 3 to a type
scheme. Type environments are used only during type inference, not at run time.
2
Nano‐ML’s representation has only four of the five forms found in Typed µScheme. The fifth form,
FUNTY, is represented in nano‐ML as a nested application of type constructors function and arguments
(chunk 412b). Coding function types in this way simplifies type inference.
3
Term variables, which appear in terms (expressions) and are bound by let or lambda, stand for
values. Don’t confuse them with type variables, which stand for types. The name of a term variable
begins with a letter or symbol; the name of a type variable begins with the ASCII quote (') character.
In nano‐ML, unlike in Typed µScheme, types don’t appear in code. And types
inferred by the system are guaranteed to be well formed, so the type system doesn’t
need formation rules or kinds. (An ill‐formed type may appear in a unit test, but in
that case the test just fails; no other checking is needed.) Kinds are used again in
µML (Chapter 8) and in full ML.
These interpretations are all related and mutually consistent. They all appear in
the math, and some appear in my code. The interpretation I use most is the func‐
tion from types to types. Such a function θ is a substitution if it preserves type
constructors and constructor application:
ML and type To be a substitution, a function from types to types must meet one other condition:
inference
• The set {α | θα 6= α} must be finite. This set is the set of variables substi‐
410 tuted for. It is called the domain of the substitution, and it is written dom θ .
Such a function is defined by tysubst in Chapter 6 on page 371; its inner function
subst has all the properties claimed above (Exercise 3).
Substitution determines when τ ′ is an instance of τ : like Milner (1978), we write
τ ′ ⩽ τ if and only if there exists a substitution θ such that τ ′ = θτ . The instance
relation τ ′ ⩽ τ is pronounced in two ways: not only “τ ′ is an instance of τ ” but also
“τ is at least as general as τ ′ .”
The instance relation is extended to type schemes: τ ′ ⩽ ∀α1 , . . . , αn . τ if and
only if there exists a substitution θ such that dom θ ⊆ {α1 , . . . , αn } and θτ = τ ′ .
The first condition says that the instantiating substitution θ may substitute only for
type variables that are bound by the ∀.
To instantiate a type scheme σ = ∀α1 , . . . , αn . τ is to choose a τ ′ ⩽ σ . An in‐
stance of σ is obtained by substituting for the type variables α1 , . . . , αn , and only
for those type variables. It’s like instantiation in Typed µScheme, except in ML, the
system instantiates each σ automatically.
In my code, a substitution is represented as a finite map from type variables
to types: an environment of type ty env. A substitution’s domain is computed by
function dom.
410a. hshared utility functions on HindleyMilner types 410ai≡ (S420a) 410b ▷
type subst = ty env type subst
fun dom theta = map (fn (a, _) => a) theta dom : subst ‑> name set
To interpret a substitution as a function from type variables to types, we apply
varsubst to it:
410b. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 410a 410c ▷
As the code shows, the function defined by a substitution is total. If type variable a
is not in the domain of theta, then varsubst theta leaves a unchanged.
A substitution is most often interpreted as a function from types to types. That
interpretation is provided by function tysubst. It is almost the same as the tysubst
function in the interpreter for Typed µScheme (page 374), but because it has no
quantified types to deal with, it is simpler.
410c. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 410b 411a ▷
fun tysubst theta = tysubst : subst ‑> (ty ‑> ty)
let fun subst (TYVAR a) = varsubst theta a subst : ty ‑> ty
| subst (TYCON c) = TYCON c
| subst (CONAPP (tau, taus)) = CONAPP (subst tau, map subst taus)
in subst
end
creates them. Milner’s algorithm substitutes for one type variable at a time, then 305e
BugInType‐
composes those substitutions. To create a substitution that substitutes for a sin‐ Inference S213d
gle variable, I define an infix function |‑‑>. The expression alpha |‑‑> tau is the CONAPP 408
substitution that substitutes tau for alpha. In math, that substitution is written emptyEnv 305a
(α 7→ τ ). type env 304
find 305b
411c. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 411b 411d ▷ FORALL 408
|‑‑> : name * ty ‑> subst mkEnv 305e
infix 7 |‑‑>
type name 303
fun a |‑‑> (TYVAR a') = if a = a' then emptyEnv NotFound 305b
else bind (a, TYVAR a', emptyEnv) type ty 408
| a |‑‑> tau = bind (a, tau, emptyEnv) TYCON 408
type type_scheme
The |‑‑> function accepts any combination of α and τ . But if α appears free in τ 408
(for example, if τ = α list), then the resulting substitution θ is not idempotent. TYVAR 408
If θ is not idempotent, then θ ◦ θ 6= θ , and moreover, θα = 6 θτ . But type inference union S217b
is all about using substitutions to guarantee equality of types, and we must be sure
that every substitution we create is idempotent, so if θ = (α 7→ τ ), then θα = θτ.
If this equality does not hold, there is a bug in type inference (Exercise 2).
A final useful substitution is the identity substitution, which is represented by
an empty environment.
411d. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 411c 412a ▷
val idsubst = emptyEnv idsubst : subst
In math, the identity substitution is written θI , and it is a left and right identity of
composition: θI ◦ θ = θ ◦ θI = θ .
My representation of substitutions is simple but not efficient. Efficient imple‐
mentations of type inference represent each type variable as a mutable cell, and
they apply and compose substitutions by mutating those cells.
The types of primitive operations are written using convenience functions very
much like those from Chapter 6.
412b. hcreation and comparison of HindleyMilner types with named type constructors 412bi≡
val inttype = TYCON "int" inttype : ty
val booltype = TYCON "bool" booltype : ty
val symtype = TYCON "sym" symtype : ty
val alpha = TYVAR "a" alpha : ty
val beta = TYVAR "b" beta : ty
val unittype = TYCON "unit" unittype : ty
fun listtype ty = listtype : ty ‑> ty
CONAPP (TYCON "list", [ty]) pairtype : ty * ty ‑> ty
fun pairtype (x, y) = funtype : ty list * ty ‑> ty
CONAPP (TYCON "pair", [x, y]) asFuntype : ty ‑> (ty list * ty) option
fun funtype (args, result) =
CONAPP (TYCON "function", [CONAPP (TYCON "arguments", args), result])
fun asFuntype (CONAPP (TYCON "function",
[CONAPP (TYCON "arguments", args), result])) =
SOME (args, result)
| asFuntype _ = NONE
In Typed µScheme, you may define and use quantified types anywhere, but you
must write type‑lambda and @ everywhere. As shown in Chapter 6, writing these
type abstractions and type applications explicitly is tiresome. And in ML, type ab‐
straction and type application are done for you; the only downside is that ML can
express fewer types than Typed µScheme. In ML, the ∀ quantifier appears only
in a type scheme, never in a type, and a function cannot expect an argument of a
quantified type.
To fill in the missing type abstractions and instantiations, nano‐ML’s type sys‐
tem does more work than a type checker for Typed µScheme:
• When a value is polymorphic, the code doesn’t say how to instantiate it; the
type system has to figure out a type at which the value should be instantiated.
• When a function is defined, the code doesn’t state the types of its arguments;
the type system has to figure out a type for each argument.
instantiate (use name)
name in Γ e (or x)
σ τ
generalize (bind name with val or let)
Figure 7.2: Relationship between type schemes and types
The types can be figured out because in the Hindley‐Milner type system, ∀ cannot §7.4
appear just anywhere. In particular, it cannot appear in the type of an expression: Type system
every welltyped expression has a monotype, although the monotype may have free for nanoML
type variables. Only a name bound in the environment may have a polytype. When a
name from Γ is used as an expression, its type scheme is instantiated to give it a 413
monotype. This instantiation amounts to an implicit @ operation. When a name
is bound by val or let, the type of its right‐hand side is generalized to make a type
scheme, which is then put into the environment. This generalization amounts to
an implicit type‑lambda operation. Instantiation and generalization are depicted
in Figure 7.2. (There’s also a sidebar on page 435.)
When a type is instantiated, what types should be substituted for the type vari‐
ables? When a function’s type is determined, what types should the arguments
have? Luckily, these questions don’t have to be answered right away. Nano‐ML’s
type system can be described by nondeterministic rules that show a type τ without
saying how τ is computed. Those rules take up the rest of this section. In Sections
7.5.1 and 7.5.2, the nondeterministic rules are refined into new rules that specify a
deterministic type‐inference algorithm.
The typing judgment for an expression has the form Γ ` e : τ , meaning that
given type environment Γ, expression e has type τ . An expression may have more
than one type; for example, the empty list has many types, and the judgments
Γ ` LıTERAL(NıL) : int list and Γ ` LıTERAL(NıL) : bool list are both valid.
A use of a variable is well typed if the variable is bound in the environment.
The variable’s type is not fully determined; it may have any type that is an instance
of its type scheme.
Γ(x) = σ τ ⩽σ
(VAR)
Γ`x:τ
Unlike our rules for operational semantics, this rule does not specify a determin‐
istic algorithm. Any τ that is an instance of σ is acceptable, and the rule does not
say how to find the “right” one. A τ is determined by the algorithm in Section 7.5.2,
which finds a most general τ (Exercise 5).
CONAPP 408
A conditional expression is well typed if the condition is Boolean and the two
eqTycon 409a
branches have the same type τ . The conditional expression also has type τ . type ty 408
TYCON 408
Γ ` e1 : bool Γ ` e2 : τ Γ ` e3 : τ TYVAR 408
(IF)
Γ ` ıF(e1 , e2 , e3 ) : τ
Γ ` ei : τi , 1 ≤ i ≤ n
(BEGıN)
Γ ` BEGıN(e1 , . . . , en ) : τn
(EMPTYBEGıN)
Γ ` BEGıN() : unit
An application is well typed if the function has arrow type, and if the types and
number of actual parameters match the types and number of formal parameters
on the left of the arrow.
Γ ` ei : τi , 1≤i≤n Γ ` e : τ1 × · · · × τn → τ
7 Γ ` APPLY(e, e1 , . . . , en ) : τ
(APPLY)
A function is well typed if, in an environment that binds each formal parameter
ML and type to its type, the function’s body is well typed. The type of the function is formed from
inference the types of its formal parameters and its body.
414 Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
(LAMBDA)
Γ ` LAMBDA(hx1 , . . . , xn i, e) : τ1 × · · · × τn → τ
Like the VAR rule, the LAMBDA rule is nondeterministic; types τ1 , . . . , τn aren’t in
the syntax, and the rule doesn’t say what they should be.
In the LAMBDA rule, the notation {xi 7→ τi } is shorthand for {xi 7→ ∀.τi };
each τi is converted into a type scheme by wrapping it in an empty ∀. The type
scheme ∀.τi has only one instance, which is τi itself. Therefore, when xi is used
in e, it always has the same type. This rule restricts the set of functions that can
be given types: an ML programmer cannot define a function that requires its argu‐
ments to be polymorphic. No matter how polymorphic an actual parameter may
be, inside the function the formal parameter has just one type. The restriction helps
make type inference decidable.
The restriction can be illustrated by comparing two bindings. If empty‑list is
defined as a global variable and is bound to the empty list, then it has a polymor‐
phic type scheme, and both an integer and a boolean can be consed onto it. But if
empty‑list is defined as a formal parameter, then no matter what actual parame‐
ter it is eventually bound to, it cannot be polymorphic:
414. htranscript 402i+≡ ◁ 409b 415 ▷
‑> (val empty‑list '())
() : (forall ['a] (list 'a))
‑> (val p (pair (cons 1 empty‑list) (cons #t empty‑list)))
(PAIR (1) (#t)) : (pair (list int) (list bool))
‑> (val too‑polymorphic
(lambda (empty‑list) (pair (cons 1 empty‑list) (cons #t empty‑list))))
type error: cannot make int equal to bool
Because the val definition form corresponds to a let, the difference shown here
is usually called the difference between a lambda‐bound variable and a let‐bound
variable. The difference can be formalized in a typing rule.
The simplest possible rule describes MLET, a restricted form of LET that binds
a single variable. When x is bound in Γ, x is given a type scheme that may quantify
over a nonempty set of type variables and so may be polymorphic:
The set {α1 , . . . , αn } is the difference of two sets computed with ftv, a function
that finds free type variables. Operationally, a type checker first finds τ ′ , the type
of e′ , but it doesn’t simply extend Γ with {x 7→ τ ′ }. Instead, using ∀, it closes
over the free type variables of τ ′ that are not also free type variables of types in Γ.
Milner discovered that if a type variable isn’t mentioned in the environment, it can
be instantiated any way you want, and it can even be instantiated it differently at
different uses of x. Closing over such type variables might give x a polymorphic
type scheme. For example, in the following variation on too‑poly, the let‐bound
variable empty‑list gets a polymorphic type scheme, and when the polymorphic
empty‑list is used, it is instantiated once with int and once with bool.
415. htranscript 402i+≡ ◁ 414 417a ▷
‑> (val not‑too‑polymorphic
(let ([empty‑list '()])
(pair (cons 1 empty‑list) (cons #t empty‑list))))
§7.4
(PAIR (1) (#t)) : (pair (list int) (list bool))
Type system
If let‐bound variables might be polymorphic, why not λ‐bound variables? for nanoML
λ‐bound variables can’t be made polymorphic because the type checker doesn’t
know what type of value a λ‐bound variable might stand for—it could be any actual 415
parameter. By contrast, the type checker knows exactly what type of value a let‐
bound variable stands for, because it is right there in the program: it is the type
of e′ . If e′ could be polymorphic (because it has type variables that don’t appear
in the environment), that polymorphism can be made explicit in the type scheme
associated with x.
The polymorphic MLET is sometimes called “Milner’s let.” Milner’s let is type‐
checked with the help of a new operation on types: generalization. Generalization
is defined by function generalize, which takes as argument a type τ and a set of
constrained type variables A:
Often A is the set of type variables that appear free in a type environment, e.g.,
A = ftv(Γ). As suggested in Figure 7.2 on page 413, generalization is like an in‐
verse of instantiation: for any τ and Γ, it’s true that τ ⩽ generalize(τ, ftv(Γ)).
Function generalize is implemented in code chunk 434b.
Using generalize, the rule for Milner’s let is written as follows:
Γ ` ei : τi , 1 ≤ i ≤ n
σi = generalize(τi , ftv(Γ)), 1 ≤ i ≤ n
Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ
(LET)
Γ ` LET(hx1 , e1 , . . . , xn , en i, e) : τ
The typing rule for LETREC is similar, except that the ei ’s are checked in type
environment Γ′ , which itself contains the bindings for the xi ’s. Environment Γ′ is cons P 440a
P
defined using the set {τ1 , . . . , τn }, and each type τi is in turn computed using Γ′ .
pair
Γ′ = Γ{x1 7→ τ1 , . . . , xn 7→ τn }
Γ′ ` ei : τi , 1 ≤ i ≤ n
σi = generalize(τi , ftv(Γ)), 1 ≤ i ≤ n
Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ . (LETREC)
Γ ` LETREC(hx1 , e1 , . . . , xn , en i, e) : τ
Within Γ′ , the variables xi are not given polymorphic type schemes. Only once all
the types of the ei ’s are fixed can the types of the xi ’s be generalized and used to
compute the type of e. A LETREC defines a nest of mutually recursive functions, and
because generalize is not applied until the types of all the functions are computed,
these functions are not polymorphic when used in each other’s definitions—they
are polymorphic only when used in e. This rule can surprise even experienced ML
7 programmers; in the presence of letrec, functions that look polymorphic may be
less polymorphic than you expected.
A rule for LETſTAR would be annoying to write down directly: there is too much
bookkeeping. Instead, LETſTAR is treated as syntactic sugar for a nest of LETs.
ML and type
inference Γ ` LET(hx1 , e1 i, LETſTAR(hx2 , e2 , . . . , xn , en i, e)) : τ n>0
(LETſTAR)
416 Γ ` LETſTAR(hx1 , e1 , . . . , xn , en i, e) : τ
Γ`e:τ
(EMPTYLETſTAR)
Γ ` LETſTAR(hi, e) : τ
hVAL(it, e), Γi → Γ′
(EXP)
hEXP(e), Γi → Γ′
As another example, length has many types and many type schemes, but only one
principal type scheme:
417c. hprincipaltypes.nml 417bi+≡ ◁ 417b
(check‑type length ((list int) ‑> int)) ; pass
(check‑principal‑type length ((list int) ‑> int)) ; FAIL
(check‑type length (forall ['a] ((list (list 'a)) ‑> int))) ; pass
(check‑principal‑type length (forall ['a] ((list (list 'a)) ‑> int))) ; FAIL
(check‑type length (forall ['a] ((list 'a) ‑> int))) ; pass
(check‑principal‑type length (forall ['a] ((list 'a) ‑> int))) ; pass
The nondeterministic typing rules don’t specify an algorithm that can decide if a
nano‐ML term has a type, let alone find a principal type. That is, given Γ and e,
the rules don’t say how to find a τ such that Γ ` e : τ . The LAMBDA rule doesn’t
specify the types of the formal parameters, and the VAR rule doesn’t specify which
instance of σ to use as τ . But these rules can be used in an algorithm if the algorithm
uses a trick: whenever a type is unknown, the algorithm identifies that type with
fresh type variable. For example, when a LAMBDA takes an argument xi , xi ’s type
is recorded as αi , where αi is a new type variable that is not used anywhere else in
the program. The αi stands for an unknown type, and the way xi is used might tell
us something about it. For example, if xi were added to 1, that would tell us that αi
has to be equal to int. Eventually int would be substituted for αi .
How does an algorithm discover what type to substitute for each fresh type vari‐
able? There are two good methods:
• The first method is the method of explicit substitutions. When the type
checker wants two types to be equal, it calls ML function unify(τ1 , τ2 ). A uni
fication algorithm returns a θ such that θ(τ1 ) = θ(τ2 ); substitution θ is called
a unifier of τ1 and τ2 . These substitutions are composed to implement type
inference: given Γ and e, the algorithm can find θ and τ such that θΓ ` e : τ .
The method of explicit substitutions is described by simple mathematics and
ML and type • The second method is the method of typeequality constraints. When the type
inference checker wants two types τ1 and τ2 to be equal, it doesn’t unify them right
away. Instead, it remembers the constraint τ1 ∼ τ2 , which says that τ1 must
418 equal τ2 . If the type checker needs two or more such constraints, it conjoins
them into a single constraint using logical and, as in C1 ∧C2 . The constraints
are used in a judgment of the form C, Γ ` e : τ .
The method of explicit constraints is described by more elaborate mathe‐
matics and is not as easy to prove correct, but the composition operation ∧
is associative and commutative, so unlike substitutions, constraints can be
composed in any order—there’s no wrong way to do it. Using constraints,
most of type inference is not so hard to get right.
In the method of explicit constraints, unification happens lazily: when it sees
a LET binding or a VAL binding, the type checker calls on a constraint solver to
produce a substitution that makes the constraints true. The constraint solver
does the same job as unify, and solving constraints is only a little bit more
complicated than unifying types. And with a constraint solver, implement‐
ing type inference itself is infinitely easier. For this reason, the method of
explicit constraints is the one that I recommend.
Both methods of type inference are justified by the same principle: if in a valid
derivation, we substitute for free type variables, the new derivation is also valid.
D θD
That is, if is a valid derivation, then for all substitutions θ ,
Γ`e:τ θΓ ` θe : θτ
is a valid derivation.
Which method should you study? It depends what you want to do.
• If you want to prove that type inference is consistent with the nondeterminis‐
tic type system in Section 7.4.5 (Exercise 11), study explicit substitutions—the
proof is much easier.
This new rule is sound, which means that whenever there is a derivation us‐
ing the new ıF rule, there is a corresponding derivation using the original ıF rule.
A real proof of soundness is beyond the scope of this book (Exercise 11), but to help
you understand how the system works, here is a hand‐waving argument: To prove
soundness, we rewrite derivations systematically so that if we are given a deriva‐
tion in the new system, we can rewrite it into a derivation in the old system. In the
case of the IF rule, we rewrite (θ ′ ◦ θ ◦ θ3 ◦ θ2 ◦ θ1 ) Γ as Γ̃, (θ ′ ◦ θ ◦ θ3 ◦ θ2 ) τ1 as τ̃1 ,
and so on. For example, if θ1 Γ ` e1 : τ1 , we apply substitution (θ ′ ◦ θ ◦ θ3 ◦ θ2 ) to
both sides, rewrite, and the premise becomes equivalent to Γ̃ ` e1 : τ̃1 . A similar
rewriting of all the premises enables us to apply the original ıF rule to draw the
conclusion.
Although the new ıF rule is sound, and it has a clear operational interpreta‐
tion, it requires a lot of explicit substitutions; it’s a bookkeeping nightmare. But the
bookkeeping can be reduced by a trick: extend the typing judgment to lists of ex‐
pressions and types. Write θΓ ` e1 , . . . , en : τ1 , . . . , τn as an abbreviation for a set
of n separate judgments: θΓ ` e1 : τ1 , . . . , θΓ ` en : τn . When n = 1, this judg‐
ment degenerates to θΓ ` e1 : τ1 . For n > 1, finding the common substitution θ
requires combining substitutions from different judgments.
θΓ ` e1 : τ1 θ′ (θΓ) ` e2 , . . . , en : τ2 , . . . , τn
(TYPEſOF)
(θ′ ◦ θ) Γ ` e1 , . . . , en : θ′ τ1 , τ2 , . . . , τn
This new judgment can be used to reduce the number of substitutions in any rule
that has multiple subexpressions, like IF. For example, the application rule can be
written like this:
θΓ ` e, e1 , . . . , en : τ̂ , τ1 , . . . , τn
θ′ (τ̂ ) = θ′ (τ1 × · · · × τn → α), where α is fresh .
(APPLY)
(θ′ ◦ θ) Γ ` APPLY(e, e1 , . . . , en ) : θ′ α
The most difficult rule to express using explicit substitutions is probably LE‐
TREC. The nondeterministic rule is
Γ′ = Γ{x1 7→ τ1 , . . . , xn 7→ τn }
Γ′ ` ei : τi , 1 ≤ i ≤ n
σi = generalize(τi , ftv(Γ)), 1 ≤ i ≤ n
Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ . (LETREC)
Γ ` LETREC(hx1 , e1 , . . . , xn , en i, e) : τ
The rule with explicit substitutions is
To find that better way, we return to the nondeterministic type system of Sec‐
tion 7.4.5. In a rule like
Γ ` e1 : bool Γ ` e2 : τ Γ ` e3 : τ ,
(IF)
Γ ` ıF(e1 , e2 , e3 ) : τ
expression e1 angelically has the right type bool, and e2 and e3 angelically have the
same type τ . Instead of requiring angels, the method of explicit constraints allows
each ei to have whatever type it wants, which I’ll call τi . Then the type checker
insists that τ1 must equal bool and τ2 must equal τ3 . Its insistence is recorded in
an explicit constraint C , which is added to the typing judgment as part of the typing
context. For the IF rule, the constraint C is τ1 ∼ bool ∧ τ2 ∼ τ3 . Operators ∼ and ∧
are explained below.
Using explicit constraints, a typing judgment has the form C, Γ ` e : τ , which
means “assuming the constraint C is satisfied, in environment Γ term e has type τ .”
Constraints are formed by conjoining simple equality constraints:
Not every judgment requires a constraint, so to keep the math and the code uni‐
form, such a judgment uses a third form of constraint:
• The trivial constraint has the form T, and it is always considered satisfied.
The trivial constraint is a left and right identity of ∧. The constraint may be
pronounced “trivial” or “true.”
C ::= τ1 ∼ τ2 C1 ∧ C2 T.
If the type system derives C, Γ ` e : τ , the constraint captures conditions that
are sufficient to ensure that term e has type τ . And if constraint C is satisfied, then
erasing constraints produces a derivation of Γ ` e : τ that is valid in the original,
nondeterministic type system. Operationally, constraint C , like type τ , is an output
from the type checker; the inputs are term e and environment Γ.
Using constraints, we can write a deterministic IF rule. The rule not only pro‐
duces new constraints τ1 ∼ bool and τ2 ∼τ3 ; it also remembers old constraints used
§7.5
to give types to the subexpressions e1 , e2 , and e3 . “Old” constraints propagate from
From typing rules
the premises of a rule to the conclusion.
to type inference
C1 , Γ ` e1 : τ1 C2 , Γ ` e2 : τ2 C3 , Γ ` e3 : τ3 421
(IF)
C1 ∧ C2 ∧ C3 ∧ τ1 ∼ bool ∧ τ2 ∼ τ3 , Γ ` ıF(e1 , e2 , e3 ) : τ2
The conclusion of this rule conjoins three old constraints with two new simple
equality constraints. For the ıF expression to have a type, all the constraints needed
to give types to e1 , e2 , and e3 must be satisfied. And so must constraints τ1 ∼ bool
and τ2 ∼ τ3 . If all the constraints are satisfied, which implies that τ1 = bool and
τ2 = τ3 , then the rule is equivalent to the original IF rule.
Constraints require much less bookkeeping than do the substitutions in Sec‐
tion 7.5.1, but a typing judgment that describes lists of expressions and types is
still worth defining. Judgment C, Γ ` e1 , . . . , en : τ1 , . . . , τn expresses the ef‐
fects of n separate judgments, where C is the conjunction of the constraints of the
individual judgments:
C1 , Γ ` e1 : τ1 · · · Cn , Γ ` en : τn .
(TYPEſOF)
C1 ∧ · · · ∧ Cn , Γ ` e1 , . . . , en : τ1 , . . . , τn
This judgment can help simplify some rules, like the IF rule:
C, Γ ` e1 , e2 , e3 : τ1 , τ2 , τ3 . (IF)
C ∧ τ1 ∼ bool ∧ τ2 ∼ τ3 , Γ ` ıF(e1 , e2 , e3 ) : τ2
The same judgment is used in the application rule, but the application rule also
does something new. In an application, the function must have an arrow type, so
if an expression e of type τ̂ appears in the function position, τ̂ must be an arrow
type. But what arrow type? The argument types are the types τ1 , . . . , τn of e’s actual
parameters, but what is the result type? Because τ̂ might itself be a type variable, we
can’t always know. So to stand in for the result type, the type checker uses a fresh
type variable α, whose ultimate identity will be determined by a new constraint.
The new constraint says that type τ̂ must be equal to τ1 × · · · × τn → α:
C, Γ ` e, e1 , . . . , en : τ̂ , τ1 , . . . , τn α is fresh . (APPLY)
C ∧ τ̂ ∼ τ1 × · · · × τn → α, Γ ` APPLY(e, e1 , . . . , en ) : α
Again, if the constraints are satisfied, the rule is equivalent to the original.
These rules are enough to build an example derivation. The example uses a
type environment Γ that contains these bindings:
This Γ has no free type variables. Derivation of a type for (+ 1 2) looks roughly like
this, where α10 is a fresh type variable:
···
T, Γ ` + : int × int → int T, Γ ` 1 : int T, Γ ` 2 : int
.
T ∧ T ∧ T ∧ int × int → int ∼ int × int → α10 , Γ ` (+ 1 2) : α10
Substituting int for α10 yields
All the constraints are satisfied, and if they are erased, what’s left is a derivation in
the original, nondeterministic system.
ML and type
inference Converting nondeterministic rules to use constraints
• If the original rule uses the same type τ in more than one place, give each use
its own name (like τ2 and τ3 ), and introduce constraints forcing the names
to be equal.
• If the original rule uses τ but does not specify what τ is, represent τ by a
fresh type variable.
The first technique is illustrated by the IF rule. The second technique can be illus‐
trated by converting the nondeterministic VAR rule to use explicit constraints. The
nondeterministic rule says
Γ(x) = σ τ ⩽ σ.
(VAR)
Γ`x:τ
By definition, τ ′ ⩽ σ when σ = ∀α1 , . . . , αn .τ and some (unknown) types are
substituted for the α1 , . . . , αn . For the unknown types, the rule uses fresh type
variables α1′ , . . . , αn
′
.
Γ(x) = ∀α1 , . . . , αn .τ
α1′ , . . . , αn′ are fresh and distinct
(VAR)
T, Γ ` x : ((α1 7→ α1′ ) ◦ · · · ◦ (αn 7→ αn′ )) τ
Converting from a monotype τ with free type variables to a type scheme σ with an 423
explicit forall is the most challenging part of type inference; this conversion is
called generalization.
Generalization is at its simplest in the rule for a VAL binding:
C, Γ ` e : τ
θC is satisfied θΓ = Γ
σ = generalize(θτ, ftv(Γ)) .
(VAL)
hVAL(x, e), Γi → Γ{x 7→ σ}
• Generalize the type θτ to form the type scheme σ , which becomes the type
of x in a new environment Γ{x 7→ σ}.
Assuming there is a valid derivation of the first premise C, Γ ` e : τ , the rule works
because for any θ ,
• If the original system can derive type θτ for e, that type can safely be gener‐
alized.
The VAL rule illustrates the key ideas underlying constraint‐based inference of
polymorphic type schemes: cons P 440a
Name singleton is therefore added to Γ with type scheme ∀α15 .α15 → α15 list,
which we prefer to write in canonical form as ∀α.α → α list.
In the method of explicit substitutions, when a term has no type, type inference
fails because the type checker calls unify with two types that can’t be unified. In the
method of explicit constraints, when a term has no type, type inference fails be‐
cause the type checker produces a constraint that can’t be solved. One such con‐
straint is produced by this example:
Let’s assume that x is introduced to the environment with the monotype ∀.α18 , that
cons is instantiated with type α19 × α19 list → α19 list, and that the return type
of the lambda is type variable α20 . Then the system derives a judgment that looks
roughly like this:
α19 ×α19 list → α19 list∼α18 ×α18 → α20 , Γ ` (lambda (x) (cons x x)) : α18 → α20 .
The third simple equality α19 list ∼ α20 can be satisfied by substituting α19 list
for α20 . The first simple equality α19 ∼ α18 can be satisfied by substituting α19
for α18 or vice versa. So the full constraint is solvable if and only if the simple
equality
α19 list ∼ α19
is solvable (or equivalently, if α18 list ∼ α18 is solvable). But no possible substi‐
4
Just as in the nondeterministic system, the rule for lambda typechecks the body in an extended
environment that binds the formal parameter x to a monotype. If you wonder why I don’t show you a
rule for lambda, it’s because I want you to develop the rule yourself; see Exercise 8 on page 446.
tution for α19 can make α19 list equal to α19 .5 And after putting the unsolvable
constraint into canonical form, that’s what the interpreter reports:
425. htranscript 402i+≡ ◁ 423 426 ▷
‑> (val broken (lambda (x) (cons x x)))
type error: cannot make 'a equal to (list 'a)
• Constraint C affects type variables that are free in τ or in Γ. The type vari‐
ables that are free in τ can be eliminated by applying θ to τ . But free type
variables of Γ can’t be substituted for; that would be unsound. Instead, the
effect of θ on those type variables is captured in a new constraint C ′ . Con‐
straint C ′ is built by applying θ to the free type variables of Γ :
^
C′ = {α ∼ θα | α ∈ dom θ ∩ ftv(Γ)}.
V
The notation {. . .} says to form a single
V constraint by conjoining the con‐
straints in the set. If the set is empty, ∅ = T.
C, Γ ` e1 : τ1
7 ′
θCVis satisfied θ is idempotent
C = {α ∼ θα | α ∈ dom θ ∩ ftv(Γ)}
σ1 = generalize(θτ1 , ftv(Γ) ∪ ftv(C ′ ))
ML and type
Γ′ = Γ{x1 7→ σ1 } . (INCOMPLETE ſıMPLE LET)
inference ···
Because θ is used to form both C ′ and σ1 , it can eventually be used twice, so for
426
soundness, it must be idempotent (sidebar on the facing page).
To complete the rule, tell the type checker to use new environment Γ′ to infer
the type of the body of the let. Extending the incomplete rule to work with an
arbitrary number of bound variables x1 , . . . , xn results in this rule:
C, Γ ` e1 , . . . , en : τ1 , . . . , τn
θCVis satisfied θ is idempotent
C ′ = {α ∼ θα | α ∈ dom θ ∩ ftv(Γ)}
σi = generalize(θτi , ftv(Γ) ∪ ftv(C ′ )), 1 ≤ i ≤ n
Cb , Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ . (LET)
C ′ ∧ Cb , Γ ` LET(hx1 , e1 , . . . , xn , en i, e) : τ
The whole derivation won’t fit on a page, so let’s look at pieces. Start by assuming
that the body of the outer lambda is typechecked in an environment
• In the general case, the type checker takes the constraint C and its solu‐
tion θ , and it splits θ into two parts, so that θ = θg ◦ θl , where
is derivable. The next part of the derivation uses an instance of the LET rule with
these values for its metavariables:
C = α22 × α22 list → α22 list ∼ α21 × α23 list → α24
τ1 = α21 → α24
V 21 7→ α22 ) ◦ (α23 7→ α22 ) ◦ (α24 7→ α22 list)
θ = (α
C′ = { } = T
ftv(Γ) = {α20 }
σ1 = generalize(α22 → α22 list, ftv(Γ)) = ∀α22 .α22 → α22 list.
The body of the LET, (single (single x)), is checked in the extended envi‐
ronment Γe = Γ{single : ∀α22 .α22 → α22 list}. Each instance of single cons P 440a
gets its own type, and the typing derivation gets crowded. Abbreviating constraint
Cinner = α25 → α25 list ∼ α20 → α26 , we have
Γe (single) = ∀α22 .α22 → α22 list Γe (y) = ∀.α20
Γe (single) = ∀α22 .α22 → α22 list T, Γe ` single : α25 → α25 list T, Γe ` y : α20
T, Γe ` single : α27 → α27 list Cinner , Γe ` (single y) : α26
.
α27 → α27 list ∼ α26 → α28 ∧ Cinner , Γe ` (single (single y)) : α28
The type of the outer lambda is therefore α20 → α28 , with constraint C ′ ∧ Cb ,
which is
T ∧ α27 → α27 list ∼ α26 → α28 ∧ α25 → α25 list ∼ α20 → α26 .
This constraint is equivalent to
α27 ∼ α26 ∧ α27 list ∼ α28 ∧ α25 ∼ α20 ∧ α25 list ∼ α26 ,
which is solved by the substitution
7 (α27 7→ α20 list) ◦ (α28 7→ α27 list) ◦ (α25 7→ α20 ) ◦ (α26 7→ α25 list),
which is equivalent to the substitution
ML and type
inference (α27 7→ α20 list) ◦ (α28 7→ α20 list list) ◦ (α25 7→ α20 ) ◦ (α26 7→ α20 list),
428 The type of the outer lambda is therefore α20 → α20 list list, and at the VAL
binding, this type is generalized to the type scheme ∀α20 .α20 → α20 list list.
As shown by the examples above, the method of explicit constraints reduces type
inference to a constraint‐solving problem. Solving a constraint tells us what, if any‐
thing, to substitute for each type variable. The substitutions are used to finalize
types at LET and VAL, where potentially polymorphic names are bound. The de‐
tails are all here, and you can use them to build your own constraint solver.
A constraint is satisfied if types that are supposed to be equal actually are equal:
τ1 = τ2 C1 is satisfied C2 is satisfied
.
τ1 ∼ τ2 is satisfied C1 ∧ C2 is satisfied T is satisfied
Constraints, like types, can be substituted in, as specified by these laws:
In addition to conjunctions, a constraint solver must also solve simple equality con‐
straints of the form τ1 ∼ τ2 . Because each type may be formed in three different
ways, a simple equality constraint is formed in one of nine different ways. Nine
cases is a lot, but the code can be cut down by clever use of ML pattern matching.
And in every case, the goal is the same: find a θ such that θτ1 = θτ2 . Let’s tackle
the most tricky case first, the easy cases next, and the most involved case last.
The tricky case is one in which the left‐hand side is a type variable, giving
the constraint the form α ∼ τ2 . This constraint can be solved by the substitution
(α 7→ τ2 ), but only in some cases:
• If τ2 does not mention α, then (α 7→ τ2 )τ2 = τ2 , and also (α 7→ τ2 )α = τ2 .
Solved!
• If τ2 is equal to α, then (α 7→ α) is the identity substitution θI . Also solved.
• If τ2 mentions α but is not equal to α—for example, suppose τ2 is α list—then
the constraint α ∼ τ2 cannot be solved (Exercise 15).
Type τ2 mentions α if and only if α occurs free in τ2 . This property has to be tested;
the test is called the occurs check.
Performance of type inference
Nano‐ML’s type inference is designed for clarity, not performance. If you write
Suppose the left‐hand side of the constraint is not a type variable, but the right‐
hand side is. That is, the constraint has the form τ1 ∼ α. This constraint has the
same solutions as α∼τ1 ; the solver can swap the two sides and call itself recursively.
If neither side is a type variable, then each side must be a type constructor or
a type application (TYCON or CONAPP). If the left is a constructor and the right is an
application, or vice versa, the constraint can’t be solved. And if both sides are type
constructors, substitution leaves them unchanged, so a constraint of the form µ∼µ
is solved by the identity substitution, and a constraint of the form µ ∼ µ′ , where
µ 6= µ′ , cannot be solved.
The most complicated case is a constraint in which both sides are constructor
applications. Because every substitution must preserve the structure of a construc‐
tor application (see the substitution laws on page 409), such a constraint can be
broken down into a conjunction of smaller constraints:
Using the ideas above, let’s solve the constraint from (cons 1 '()),
T ∧ (T ∧ α11 × α11 list → α11 list ∼ int × α12 list → α13 ). (7.1)
This constraint is big enough to be interesting, but for a complete, formal deriva‐
tion, it’s a little too big. So let’s solve it informally.
§7.5
The constraint is a conjunction, so we first solve the left conjunct, which is T.
From typing rules
This conjunct is solved by the identity substitution θI , which we then apply to the
to type inference
right conjunct
431
T ∧ α11 × α11 list → α11 list ∼ int × α12 list → α13 . (7.2)
The identity substitution leaves this conjunct unchanged, and we continue solving
recursively. The same steps lead us to solve
α11 × α11 list → α11 list ∼ int × α12 list → α13 . (7.3)
This simple equality constraint has CONAPP (with the → constructor) on both sides.
We use the SOLVECONAPPCONAPP rule to convert this constraint to
(→ ∼ →) ∧ (α11 × α11 list ∼ int × α12 list ∧ α11 list ∼ α13 ). (7.4)
The left conjunct, (→ ∼ →), has two equal type constructors and so is solved by θI ,
which, when applied to the right conjunct, leaves it unchanged. So we solve
α11 × α11 list ∼ int × α12 list ∧ α11 list ∼ α13 . (7.5)
Finally we have a case with a type variable on the left, and constraint 7.9 is solved
by θ1 = α11 7→ int. We then apply θ1 to the constraint α11 list ∼ α12 list,
yielding
int list ∼ α12 list. (7.10)
Let’s not go through all the steps; constraint 7.10 is solved by θ2 = α12 7→ int.
Constraints 7.6, 7.7, and 7.8 are therefore solved by the composition of θ1 and θ2 ,
which is θ2 ◦ θ1 = (α12 7→ int ◦ α11 7→ int).
Now we can return to constraint 7.5. We apply θ2 ◦ θ1 to the right conjunct
α11 list ∼ α13 , yielding
int list ∼ α13 , (7.11)
which is solved by substitution θ3 = α13 7→ int list. Constraint 7.5 is therefore
solved by substitution θ3 ◦ θ2 ◦ θ1 , which is
∀α.τ becomes
Instantiation instantiate(∀α.τ , [τ ′ ]) (page 411)
τ [α 7→ τ ′ ]
int, bool, . . . Base types inttype, booltype, . . . (page 412)
τ1 × · · · × τn → τ Function type funtype([τ1 , . . . , τn ], τ ) (page 412)
7.6 THE ıNTERPRETER
In most respects, the interpreter for nano‐ML is the interpreter for µScheme (Chap‐
ter 5), plus type inference. Significant parts of type inference don’t appear here,
however, because they are meant to be exercises.
Type variables like 't136 are not suitable for use in error messages. A type scheme
like (forall ['t136] ((list 't136) ‑> int)) is unpleasant to look at, and it is
equivalent to the more readable (forall ['a] ((list 'a) ‑> int)) When a type
variable is ∀‐bound, its name is irrelevant, so function canonicalize renames
bound type variables using names 'a, 'b, and so on.
433b. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 412a 434a ▷
canonicalize : type_scheme ‑> type_scheme
newBoundVars : int * name list ‑> name list
fun canonicalize (FORALL (bound, ty)) = CONAPP 408
let fun canonicalTyvarName n = diff S217b
emptyset S217b
if n < 26 then "'" ^ str (chr (ord #"a" + n))
FORALL 408
else "'v" ^ intString (n ‑ 25) insert S217b
val free = diff (freetyvars ty, bound) intString S214c
fun unusedIndex n = member S217b
if member (canonicalTyvarName n) free then unusedIndex (n+1) else n mkEnv 305e
fun newBoundVars (index, []) = [] type name 303
reverse S219b
| newBoundVars (index, oldvar :: oldvars) =
type ty 408
let val n = unusedIndex index
TYCON 408
in canonicalTyvarName n :: newBoundVars (n+1, oldvars) type type_scheme
end 408
val newBound = newBoundVars (0, bound) tysubst 410c
in FORALL (newBound, TYVAR 408
tysubst (mkEnv (bound, map TYVAR newBound)) ty)
end
Internal function unusedIndex finds a name for a single bound type variable; it en‐
sures that the name is not the name of any free type variable.
A type variable that does not appear in any type environment or substitution is
called fresh. When a function is introduced, fresh type variables are used as the
(unknown) types of its arguments. When a polytype is instantiated, fresh type
variables are used as the unknown types that are substituted for its bound type
variables. And when a function is applied, a fresh type variable is used as its (un‐
known) result type.
Fresh type variables are created by the freshtyvar function. The function uses
7 a private mutable counter to supply an arbitrary number of type variables of the
form tn. Because a nano‐ML expression or definition never contains any explicit
type variables, the names don’t collide with other names.
ML and type 434a. hshared utility functions on HindleyMilner types 410ai+≡ (S420a) ◁ 433b 434b ▷
local freshtyvar : 'a ‑> ty
inference
val n = ref 1
434 in
fun freshtyvar _ = TYVAR ("'t" ^ intString (!n) before n := !n + 1)
end
Function generalize is called with the free type variables of some type environ‐
ment. And a type environment contains the type of every defined name, so it can
get big. To reduce the cost of searching a large environment for free type variables,
a type environment is represented in a way that enables the type checker to find
free type variables in constant time.
A representation of type environments must support these functions:
• Function freetyvarsGamma finds the type variables free in Γ, i.e., the type
variables free in any σ in Γ. It is used to get a set of free type variables to use
in generalize; when a type scheme is assigned to a let‐bound variable, only
those type variables not free in Γ may be ∀‐bound.
We use quantified types (i.e., type schemes) so we can instantiate them when we
look them up in an environment. Instantiation gives us the full effect of poly‐
morphism. Without instantiation, we wouldn’t be able to type such ML terms as
(1::nil, true::nil). Suppose we had an environment Γ with only types, not
type schemes:
§7.6
Γ = {1 : int, true : bool, nil : α list, :: : α × α list → α list}. The interpreter
When typechecking 1::nil, we would get the constraint α ∼ int. And when 435
typechecking true::nil, we would get the constraint α ∼ bool. But the con‐
junction α ∼ int ∧ α ∼ bool has no solution, and type checking would fail.
Instead, we use freshInstance to make sure that every use of a polymorphic
value (here :: and nil) has a type different from any other instance. In order to
make that work, the environment has to contain polytypes:
The constraint 't121 ∼ int ∧ int ∼ 't122 ∧ 't123 ∼ bool ∧ bool ∼ 't124
does have a solution, and the whole term has the type int list * bool list, as
desired.
functions use a representation that includes a cache of the type environment’s free bind 305d
type variables. canonicalize
433b
435a. hspecialized environments for type schemes 435ai≡ (S420a) 435b ▷
diff S217b
type type_env = type_scheme env * name set
emptyEnv 305a
An empty type environment binds no variables and has an empty cache. Look‐ emptyset S217b
type env 304
ing up a type scheme ignores the cache.
find 305b
435b. hspecialized environments for type schemes 435ai+≡ (S420a) ◁ 435a 435c ▷ FORALL 408
emptyTypeEnv : type_env freetyvars 433a
val emptyTypeEnv =
instantiate 411b
(emptyEnv, emptyset) findtyscheme : name * type_env ‑> type_scheme
intString S214c
fun findtyscheme (x, (Gamma, free)) = find (x, Gamma) type name 303
type ty 408
Adding a new binding also adds to the cache. The new cache is the union of the
type type_scheme
existing cache with the free type variables of the new type scheme σ . 408
435c. hspecialized environments for type schemes 435ai+≡ (S420a) ◁ 435b 435d ▷ TYVAR 408
union S217b
bindtyscheme : name * type_scheme * type_env ‑> type_env
In the interpreter, constraints are represented in a way that resembles the math:
the ∼ operator is ~; the ∧ operator is /\; and the T constraint is TRIVIAL.
| /\ of con * con
| TRIVIAL
ML and type infix 4 ~
inference infix 3 /\
436 (The name ~ normally stands for ML’s negation function. An unqualified ~ is rede‐
fined by this datatype definition, but negation can still be referred to by its quali‐
fied name Int.~.)
let fun subst (tau1 ~ tau2) = tysubst theta tau1 ~ tysubst theta tau2
| subst (c1 /\ c2) = subst c1 /\ subst c2
| subst TRIVIAL = TRIVIAL
in subst
end
V
The { · · · } notation is implemented by ML function conjoinConstraints.
To preserve the number and order of sub‐constraints, it avoids using foldl or
foldr.
436d. hutility functions on type constraints 436bi+≡ (S420b) ◁ 436c 437c ▷
fun conjoinConstraints [] = TRIVIAL conjoinConstraints : con list ‑> con
| conjoinConstraints [c] = c
| conjoinConstraints (c::cs) = c /\ conjoinConstraints cs
Constraint solving
The mechanism is a little weird. To make a single type out of τ1 and τ2 , so their
variables can be canonicalized together, I make the type τ1 → τ2 . What’s weird is
that there’s no function—it’s just a device to make one type out of two. When I get
the canonical version, I take it apart to get back canonical types t1' and t2'.
I don’t provide a solver; I hope you will implement one.
437b. hconstraint solving [[prototype]] 437bi≡
fun solve c = raise LeftAsExercise "solve" solve : con ‑> subst
For debugging, it can be useful to see if a substitution solves a constraint.
437c. hutility functions on type constraints 436bi+≡ (S420b) ◁ 436d
| isSolved (tau ~ tau') = eqType (tau,tau') solves : subst * con ‑> bool
asFuntype 412b
| isSolved (c /\ c') = isSolved c andalso isSolved c' canonicalize
fun solves (theta, c) = isSolved (consubst theta c) 433b
emptyset S217b
eqType 412a
7.6.4 Type inference type exp 404
FORALL 408
Type inference builds on constraint solving. It comprises two functions: typeof, freetyvars 433a
which implements the typing rules for expressions, and typdef, which implements funtype 412b
InternalError
the rules for definitions. S219e
LeftAsExercise
Type inference for expressions S213b
literal 438b
Given an expression e and type environment Γ, function typeof(e, Γ) returns a type ty 408
ty 438c
pair (τ, C) such that C, Γ ` e : τ . It uses internal functions typesof, literal, type type_env
and ty. 435a
437d. hdefinitions of typeof and typdef for nanoML and µML 437di≡ (S420b) 439a ▷ TypeError S213d
typesof 438a
typeof : exp * type_env ‑> ty * con typeString S431b
typesof : exp list * type_env ‑> ty list * con tysubst 410c
literal : value ‑> ty * con union S217b
fun typeof (e, Gamma) = ty : exp ‑> ty * con type value 405b
let hshared definition of typesof, to infer the types of a list of expressions 438ai
hfunction literal, to infer the type of a literal constant (left as an exercise)i
hfunction ty, to infer the type of a nanoML expression, given Gamma 438ci
in ty e
end
438c. hfunction ty, to infer the type of a nanoML expression, given Gamma 438ci≡ (437d)
fun ty (LITERAL n) = literal n
hmore alternatives for ty 438di
To infer the type of a variable, we use fresh type variables to create a most gen‐
eral instance of the variable’s type scheme in Γ. No constraint is needed.
438d. hmore alternatives for ty 438di≡ (438c) 438e ▷
| ty (VAR x) = (freshInstance (findtyscheme (x, Gamma)), TRIVIAL)
To infer the type of a function application, we need a rule that uses constraints.
By rewriting the nondeterministic rule as described in Section 7.5.2, we get this
rule:
C, Γ ` e, e1 , . . . , en : τ̂ , τ1 , . . . , τn α is fresh . (APPLY)
C ∧ τ̂ ∼ τ1 × · · · × τn → α, Γ ` APPLY(e, e1 , . . . , en ) : α
This rule is implemented by letting funty stand for τ̂ , actualtypes stand for
τ1 , . . . , τn , and rettype stand for α. The first premise is implemented by a call
to typesof and the second by a call to freshtyvar. The constraint is formed just as
specified in the rule.
438e. hmore alternatives for ty 438di+≡ (438c) ◁ 438d 438f ▷
| ty (APPLY (f, actuals)) =
(case typesof (f :: actuals, Gamma)
of ([], _) => raise InternalError "pattern match"
| (funty :: actualtypes, c) =>
let val rettype = freshtyvar ()
in (rettype, c /\ (funty ~ funtype (actualtypes, rettype)))
end)
A definition extends the top‐level type environment. Function typdef infers the
type of the thing defined, generalizes it to a type scheme, and adds a binding to the
environment. This step types the definition. Function typdef returns the new type
environment, plus a string that describes the type scheme of the new binding.
439a. hdefinitions of typeof and typdef for nanoML and µML 437di+≡ (S420b) ◁ 437d
fun typdef (d, Gamma) = typdef : def * type_env ‑> type_env * string
§7.6
case d
of VAL (x, e) => hinfer and bind type for VAL (x, e) for nanoML 439bi
The interpreter
| VALREC (x, e) => hinfer and bind type for VALREC (x, e) for nanoML 439ci 439
| EXP e => typdef (VAL ("it", e), Gamma)
| DEFINE (x, lambda) => typdef (VALREC (x, LAMBDA lambda), Gamma)
hextra case for typdef used only in µML S435ai
Forms EXP and DEFINE are syntactic sugar.
The cases for VAL and VALREC resemble each other. A VAL computes a type and
generalizes it.
C, Γ ` e : τ APPLY 404
θC is satisfied θΓ = Γ BEGIN 404
σ = generalize(θτ, ftv(Γ)) bindtyscheme
(VAL) 435c
hVAL(x, e), Γi → Γ{x 7→ σ} type def 405a
439b. hinfer and bind type for VAL (x, e) for nanoML 439bi≡ (439a) DEFINE 405a
let val (tau, c) = typeof (e, Gamma) EXP 405a
findtyscheme
val theta = solve c
435b
val sigma = generalize (tysubst theta tau, freetyvarsGamma Gamma) FORALL 408
in (bindtyscheme (x, sigma, Gamma), typeSchemeString sigma) freetyvarsGamma
end 435d
freshInstance
This code takes a big shortcut: it assumes that θΓ = Γ. That assumption is sound 434c
because a toplevel Γ never contains a free type variable (Exercise 10). This property freshtyvar 434a
guarantees that θΓ = Γ for any θ . funtype 412b
A VALREC is a bit more complicated. The nondeterministic rule calls for an en‐ Gamma 437d
generalize 434b
vironment that binds x to τ , but τ isn’t known until e is typechecked: IFX 404
InternalError
Γ{x 7→ τ } ` e : τ σ = generalize(τ, ftv(Γ)) . S219e
(VALREC)
hVAL‐REC(x, e), Γi → Γ{x 7→ σ} LAMBDA 404
LeftAsExercise
The rule is made deterministic by initially using a fresh α to stand for τ , then once S213b
LET 404
τ is known, adding the constraint α ∼ τ : LETREC 404
LETSTAR 404
C, Γ{x 7→ α} ` e : τ α is fresh LETX 404
θ(C ∧ α ∼ τ ) is satisfied θΓ = Γ LITERAL 404
solve 437b
σ = generalize(θα, ftv(Γ)) . TRIVIAL 436a
(VALREC with constraints)
hVAL‐REC(x, e), Γi → Γ{x 7→ σ} type type_env
435a
439c. hinfer and bind type for VALREC (x, e) for nanoML 439ci≡ (439a) typeof 437d
let val alpha = freshtyvar () typeSchemeString
S431d
val Gamma' = bindtyscheme (x, FORALL ([], alpha), Gamma)
tysubst 410c
val (tau, c) = typeof (e, Gamma') VAL 405a
val theta = solve (c /\ alpha ~ tau) VALREC 405a
val sigma = generalize (tysubst theta alpha, freetyvarsGamma Gamma) VAR 404
in (bindtyscheme (x, sigma, Gamma), typeSchemeString sigma)
end
7.6.5 Primitives
As in Typed µScheme, each primitive has a value and a type. Most of nano‐ML’s
primitives are just as in Typed µScheme; only a few are shown below. As in
7 Chapters 5 and 6, the values are defined using higher‐order functions unaryOp,
binaryOp, and arithOp, which are defined in the Supplement. The values are un‐
changed, except that errors raise BugInTypeInference, not BugInTypeChecking.
A primitive may have a polymorphic type scheme, but type schemes aren’t
ML and type
coded directly. Instead, each primitive is coded with a type that may have free
inference
type variables, and when the primitive is installed in the initial type environment,
440 its type is generalized. Types are shorter and easier to read than type schemes.
440a. hprimitives for nanoML :: 440ai≡ (S425c)
("null?", unaryOp (fn NIL => BOOLV true | _ => BOOLV false),
funtype ([listtype alpha], booltype)) ::
("cons", binaryOp (fn (a, b) => PAIR (a, b)),
funtype ([alpha, listtype alpha], listtype alpha)) ::
("car", unaryOp
(fn (PAIR (car, _)) => car
| NIL => raise RuntimeError "car applied to empty list"
| _ => raise BugInTypeInference "car applied to non‑list"),
funtype ([listtype alpha], alpha)) ::
("cdr", unaryOp
(fn (PAIR (_, cdr)) => cdr
| NIL => raise RuntimeError "cdr applied to empty list"
| _ => raise BugInTypeInference "cdr applied to non‑list"),
funtype ([listtype alpha], listtype alpha)) ::
The other primitive worth showing here is error. Its type, ∀α, β . α → β ,
tells us something interesting about its behavior. The type suggests that error
can produce an arbitrary β without ever consuming one. Such a miracle is im‐
possible; what the type tells us is that the error function never returns normally.
In nano‐ML, a function of this type either halts the interpreter or fails to terminate;
in full ML, a function of this type could also raise an exception.
440b. hprimitives for nanoML and µML :: 440bi≡ (S425c)
("error", unaryOp (fn v => raise RuntimeError (valueString v)),
funtype ([alpha], beta)) ::
The Hindley‐Milner type system has been used in many languages, but the first is
the one Milner himself worked on: Standard ML. In Standard ML, as in most other
languages based on Hindley‐Milner, a programmer can mix inferred types with
explicit types. For example, instead of type‑lambda, Standard ML allows explicit
type variables after a val or fun keyword. And instead of @, Standard ML offers a
§7.7
typeascription form (e : τ ). Where an @ form gives the type at which a polymorphic
HindleyMilner
value is instantiated, an ascription gives the type of the resulting instance. As an
as it really is
example of explicit instantiation, the following Typed µScheme code instantiates
polymorphic list functions null?, car, and cdr: 441
441a. hsum function for Typed µScheme 441ai≡
(val‑rec [sum : ((list int) ‑> int)]
(lambda ([ns : (list int)])
(if ([@ null? int] ns)
0
(+ ([@ car int] ns) (sum ([@ cdr int] ns))))))
In Standard ML, type ascription can be used to give the types of the instances of the
corresponding functions, null, hd, and tl, as well as the parameter ns:
441b. hsum function for Standard ML 441bi≡
val rec sum =
fn (ns : int list) =>
if (null : int list ‑> bool) ns then 0
else (hd : int list ‑> int) ns + sum ((tl : int list ‑> int list) ns)
The Hindley‐Milner type system is just a starting point. A good next step is
functional language Haskell, whose type system combines Hindley‐Milner type in‐
ference with operator overloading. Most implementations of Haskell also support
more general polymorphism; for example, the Glasgow Haskell Compiler provides
an explicit forall that supports lambda‐bound variables with polymorphic types.
7.8 SUMMARY
alpha 412b
beta 412b
Type inference changed the landscape of functional languages. Milner (1978) pre‐ binaryOp S421d
sented his type‐inference algorithm just 4 years after Reynolds (1974) described the booltype 412b
polymorphic calculus underlying Typed µScheme. Over 40 years later, although it BOOLV 405b
BugInType‐
has been extended and elaborated in many innovative ways, Milner’s type infer‐ Inference S213d
ence remains a sweet spot in the design of typed languages. funtype 412b
Milner’s original formulation manipulates only substitutions generated by uni‐ listtype 412b
fication. From unifications to constraints is just a small step, but the constraint‐ NIL 405b
PAIR 405b
based, “generate‐and‐solve” model of type inference has proven resilient and ex‐ RuntimeError
tensible. For type inference today, it is the model of choice. S213b
unaryOp S421d
valueString 307b
7.8.1 Key words and phrases
POLYTYPE A TYPE ſCHEME that may be instantiated in more than one way. That is,
one that quantifies over a nonempty list of type variables. In ML, only a let‐
bound variable may have a polytype. Compare with MONOTYPE.
PRıNCıPAL TYPE A type that can be ascribed to an expression such that any other
type ascribable to the expression is an ıNſTANCE of the principal type.
In other words, a MOſT GENERAL type of an expression. Also used as short‐
hand for PRıNCıPAL TYPE ſCHEME, which is similar. In ML, principal type
schemes are unique up to renaming of bound type variables.
SUBſTıTUTıON A finite map from type variables to TYPEſ. Also defines maps from
types to types, CONſTRAıNTſ to constraints, and others.
TYPE In ML, a type formed using type constructors, type variables, and function
arrows. Does not include any quantification. Compare with TYPE ſCHEME.
The original work on the Hindley‐Milner type system appears in two papers. Milner
(1978) emphasizes the use of polymorphism in programming, and Hindley (1969)
emphasizes the existence of principal types. Milner describes Algorithm W, which
is the “method of explicit substitutions” in this chapter. Damas and Milner (1982)
show that Algorithm W finds the most general type of every term.
Odersky, Sulzmann, and Wehr (1999) present HM(X ), a general system for im‐
§7.9
plementing Hindley‐Milner type inference with abstract constraints. This system
Exercises
is considerably more ambitious than nano‐ML; it allows a very broad class of con‐
straints, and it decouples constraint solving from type inference. Pottier and Rémy 443
(2005) use the power of HM(X ) to explore a number of extensions to ML. Their
tutorial includes code written in the related language OCaml.
Vytiniotis, Peyton Jones, and Schrijvers (2010) argue that as type systems grow
more sophisticated, Milner’s LET rule makes it harder, not easier, to work with the
associated constraints. They recommend that by default, the types of LET‐bound
names should not be generalized.
In the presence of mutable reference cells, Milner’s LET rule is unsound. While
the unsoundness can be patched by various annotations on type variables, a better
approach is to generalize the type of a LET‐bound variable only if the expression
to which the variable is bound is a syntactic value, such as a variable, a literal, or
a lambda expression (Wright 1995).
Cardelli (1997) provides a general tutorial on type sytems. Cardelli (1987) has
also written a tutorial specifically on type inference; it includes an implementation
in Modula‐2. The implementation represents type variables using mutable cells
and does not use explicit substitutions.
Peyton Jones et al. (2007) show how by adding type annotations, one can imple‐
ment type inference for types in which a ∀ quantifier may appear to the left of an
arrow—that is, types in which functions may require callers to pass polymorphic ar‐
guments. Such types are an example of higherrank types. The authors present both
nondeterministic and deterministic rules. The paper is accompanied by code, and
it repays careful study.
Material on Haskell can be found at www.haskell.org. A nice implementation
of Haskell’s type system, in Haskell, is presented by Jones (1999).
7.9 EXERCıſEſ
The exercises are summarized in Table 7.4 on the next page. There are many that
I like, but type inference takes center stage.
• In Exercises 18 and 19 (pages 448 and 449), you implement a constraint solver
and finish the implementation of type inference. Before you tackle the con‐
straint solver, I recommend that you do Exercises 12 and 16, which will help
you solve conjunction constraints in the right way.
• Exercises 1 and 5 offer nice insights into properties of the type system.
7 1
2 to 4
Ch. 5
7.4.3
Using type inference to get a term of an unusual type.
Substitutions: understand idempotence; confirm
properties of the implementation; substitution preserves
constraint satisfaction (§7.5.3).
ML and type 5 to 7 7.4 Principal types: equivalence, uniqueness up to
inference equivalence. Most general instances of type schemes.
8 to 11 7.4, 7.5 Writing constraint‐based rules; consequences of the rules.
444 12 to 15 7.5.3 Constraints: solvability of conjunctions, soundness of rules
for the solver.
16 to 20 7.5, 7.6 Implementation of constraint solving and type inference.
21 to 23 7.6 Extending nano‐ML: pairs, a list constructor, mutable
reference cells.
24 and 25 7.6 Improving error messages; elaborating untyped nano‐ML
terms into Typed µScheme terms.
L. What is the principal type (or principal type scheme) of (lambda (x y) x)?
M. In the initial basis, what principal type scheme should be assigned to the prim‐
itive function “null?”?
N. What’s the difference, if any, between substitution θ1 ◦ θ2 and substitution
θ2 ◦ θ1 ?
O. What’s the difference, if any, between constraint C1 ∧ C2 and constraint
C2 ∧ C1 ?
P. What’s the difference, if any, between constraint τ1 ∼τ2 and constraint τ2 ∼τ1 ?
Q. For type inference, why do I recommend against implementing the method of
explicit substitutions? What’s an example of a typing rule that illustrates the §7.9
difficulties of this method? Exercises
R. Can the constraint α ∼ bool be satisfied? If so, by what substitution? What
445
about constraint α× int ∼ bool ×α? What about constraint α× int ∼ bool ×β ?
S. If constraint τ ∼ τ1 → τ2 is to be satisfied, what form must τ have?
T. If θ1 solves C1 and θ2 solves C2 , does θ2 ◦ θ1 solve C1 ∧ C2 ?
U. What’s the algorithm for solving a constraint of the form C1 ∧ C2 ?
V. In the interpreter, why does Γ have a different representation than it did in
Chapter 6?
W. Given a list of constraints C1 , . . . , Cn , what interpreter function do you call to
combine them into aV single constraint C1 ∧· · ·∧Cn ? (The combined constraint
may also be written {C1 , . . . , Cn }.)
X. When a constraint can’t be solved, what interpreter function should you call?
Y. In chunk 438e, the combination of f and actuals into a single list is a little
awkward. How does the code work? What’s the alternative? Why do you think
I coded it this way?
(a) Without using any primitives, and without using letrec, write a func‐
tion in nano‐ML that has type ∀α, β . α → β .
(b) Based on your experience, if you see a function whose result type is a
quantified type variable, what should you conclude about that function?
5. Most general instances. Prove that for any type scheme σ , there is a most general
instance τ ⩽ σ . An instance τ is a most general instance of σ if and only if
7 ∀τ ′ . τ ′ ⩽ σ =⇒ τ ′ ⩽ τ .
6. Uniqueness of principal types. Principal types are unique up to renaming of
variables.
ML and type
inference (a) Give an example of an environment and a term such that the term has
more than one principal type. Show two different principal types.
446 (b) Prove that if Γ ` e : τP and Γ ` e : τP′ and both τP and τP′ are principal
types for e in Γ, then τP′ can be obtained from τP by renaming variables.
Use the definition of principal type on page 416.
(a) Prove that renaming a bound type variable in σ does not change its set
of instances.
(b) Prove that reordering bound type variables in σ does not change its set
of instances. It suffices to prove that adjacent type variables can be
swapped without changing the set of instances.
(c) Prove that if a type variable appears in the prefix of σ but does not ap‐
pear in the body, then removing that variable from the prefix does not
change the set of instances.
(d) Conclude that if type schemes σ and σ ′ are considered equal according
to the Definition of Standard ML, then they have the same instances, and
so they are also considered equal by check‑principal‑type.
Γ ` ei : τi , 1 ≤ i ≤ n .
(BEGıN)
Γ ` BEGıN(e1 , . . . , en ) : τn
Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ . (LAMBDA)
Γ ` LAMBDA(hx1 , . . . , xn i, e) : τ1 × · · · × τn → τ
9. Recursive definitions. In nano‐ML, the parser enforces the restriction that the
right‐hand side of a val‑rec definition must be a lambda. But the typing
rules permit a definition of the form (val‑rec x x). Given this definition,
what type scheme σ do the rules say is inferred for x? Is that σ inhabited by
any values? In other words, is there a value that could be stored in ρ that is
consistent with that σ ?
10. Absence of free type variables in toplevel type environments. Prove that in
nano‐ML, a top‐level type environment never contains a free type variable. §7.9
Your proof should be by induction on the sequence of steps used to create Γ: Exercises
447
(a) Prove that an empty type environment contains no free type variables.
(b) Using the code in chunk S425c, show that if Γ contains no free type vari‐
ables, then addPrim((x, p, τ ), (Γ, ρ)) returns a pair in which the
new Γ′ also contains no free type variables.
(c) Show that if Γ contains no free type variables, and if Γ′ is specified by
hVAL(x, e), Γi → Γ′ , then Γ′ contains no free type variables.
(d) Show that if Γ contains no free type variables, and if Γ′ is specified by
hVAL‐REC(x, e), Γi → Γ′ , then Γ′ contains no free type variables.
11. Consistency of type inference with nondeterministic rules. Prove that whenever
there is a derivation of a judgment θΓ ` e : τ using the rules for explicit
substitutions in Section 7.5.1, then if Γ′ = θΓ, there is also a derivation of
Γ′ ` e : τ using the nondeterministic rules in Section 7.4.5.
To prove this property for nano‐ML would be tedious; nano‐ML has too many
syntactic forms. Instead, prove it for a subset, which I’ll call “pico‐ML,” and
which has just these forms: lambda, function application, variable, and let.
Both let and lambda bind exactly one name, and a function application has
exactly one argument.
What about the other direction? If there is a derivation using the nondeter‐
ministic rules, is there a corresponding derivation using explicit substitu‐
tions? Yes, but the corresponding derivation might not derive the same type.
The most we can say about a type derivable using the nondeterministic rules
is that it must be an instance of the type derived using type inference with
explicit substitutions. The type derived using type inference is special, be‐
cause all other derivable types are instances of it; it is the term’s principal
type. Proving that a principal type exists and that the type‐inference algo‐
rithm finds one are problems that are beyond the scope of this book.
(a) Using the proof system in Section 7.5.3, prove that if the constraint
C1 ∧ C2 is solvable, then constraints C1 and C2 are also solvable.
(b) Find a particular pair of constraints C1 and C2 such that C1 is solvable,
C2 is solvable, but C1 ∧ C2 is not solvable. Prove that C1 ∧ C2 is not
solvable.
15. Need for an occurs check. Prove that if τ2 mentions α but is not equal to α, then
there is no substitution θ such that θα = θτ2 . (Hint: Count type construc‐
tors.)
16. Practice with conjunction constraints. The most common mistake made in con‐
straint solving is to get conjunctions wrong (Section 7.5.3). Before you tackle
a solver, this exercise asks you to develop some examples and to verify that
the naïve approach works sometimes, but not always.
(a) Find two constraints C1 and C2 and substitutions θ1 and θ2 such that
• C1 has a free type variable,
• C2 has a free type variable,
• θ1 solves C1 ,
• θ2 solves C2 , and
• θ2 ◦ θ1 solves C1 ∧ C2 .
(b) Find two constraints C1 and C2 and substitutions θ1 and θ2 such that
• C1 has a free type variable,
• C2 has a free type variable,
• θ1 solves C1 ,
• θ2 solves C2 , and
• θ2 ◦ θ1 does not solve C1 ∧ C2 .
17. Understanding a recursive solver. Page 431 discusses the solution of con‐
straint 7.1, which involves synthesizing and solving constraints 7.2 to 7.11.
List the numbered constraints in the order that they would be solved by a recur‐
sive solver.
18. Implementing a constraint solver. Using the ideas in Section 7.5.3, implement a
function solve which takes as argument a constraint of type con and returns
an idempotent substitution of type subst. (If a substitution is created using
only the value idsubst and the functions |‑‑> and compose, and if |‑‑> is
used as described in Exercise 2, then the substitution is guaranteed to be
idempotent.) The resulting substitution should solve the constraint, obeying
the law
solves (solve C , C ).
If the constraint has no solution, call function unsatisfiableEquality from
chunk 437a, which raises the TypeError exception.
19. Implementing type inference. Complete the definitions of functions ty and
literal so they never raise LeftAsExercise. If your code discovers a type
error, it should raise the exception TypeError.
The function literal must give a suitable type to integer literals, Boolean
literals, symbol literals (which have type sym), and quoted lists in which all
elements have the same type (including the empty list). For example, the
value '(1 2 3) must have type int list. Values created using CLOSURE or
PRIMITIVE cannot possibly appear in a LITERAL node, so if your literal §7.9
function sees such a value, it can raise BugInTypeInference. Exercises
You will probably find it helpful to refer to the typing rules for nano‐ML,
449
which are summarized in Figures 7.5 to 7.8 on pages 450 and 451. And don’t
overlook the typesof function.
20. Constraint solving from unification. Milner’s original formulation of type in‐
ference relies on unification of explicit substitutions. To show that unifica‐
tion is as powerful as constraint solving, suppose that you have a function
unify such that given any two types τ1 and τ2 , unify(τ1 , τ2 ) returns a sub‐
stitution θ such that θτ1 = θτ2 , or if no such θ exists, raises an exception.
Use unify to implement a constraint solver.
Hint: To help convert a constraint‐solving problem into a unification prob‐
lem, try using the SOLVECONAPPCONAPP rule on page 430 in reverse.
21. Pair primitives. Extend nano‐ML with primitives pair, fst, and snd. Give the
primitives appropriate types. Function pair should be used to create pairs
of any type, and functions fst and snd should retrieve the elements of any
pair.
22. A list constructor. In nano‐ML, the most convenient way to build a large list is
by using a large expression that contains a great many applications of cons.
But as described in the sidebar on page 430, type inference using my data
structures requires time and space that is quadratic in the number of appli‐
cations of cons. The problem can be addressed through more efficient repre‐
sentations, but there is a surprisingly simple fix through language design: ex‐
tend nano‐ML with a LIST form of expression, which should work the same
way as Standard ML’s square‐bracket‐and‐comma syntax. The form should
obey this rule of operational semantics:
he1 , ρi ⇓ v1 ··· hen , ρi ⇓ vn
v = PAıR(v1 , PAıR(v2 , . . . , PAıR(vn , NıL))) .
(LıſT)
hLıſT(e1 , . . . , en ), ρi ⇓ v
And here is a nondeterministic typing rule:
Γ ` e1 : τ ··· Γ ` en : τ .
(LıſT)
Γ ` LıſT(e1 , . . . , en ) : τ list
Implement a list constructor for nano‐ML, in the following four steps:
(a) Extend the abstract syntax for exp with a case LIST of exp list.
(b) Extend the parser to accept (list e1 · · · en ) to create a LIST node.
As a model, use the parser for begin.
(c) Extend the evaluator to handle the LIST case.
(d) Extend type inference to handle the LIST case.
VAR
Γ(x) = σ τ ⩽σ
Γ`e:τ
7 IF
Γ`x:τ
Γ ` e1 : bool Γ ` e2 : τ Γ ` e3 : τ
ML and type Γ ` ıF(e1 , e2 , e3 ) : τ
inference APPLY
Γ ` ei : τi , 1≤i≤n Γ ` e : τ1 × · · · × τn → τ
450
Γ ` APPLY(e, e1 , . . . , en ) : τ
LAMBDA
Γ{x1 7→ τ1 , . . . , xn 7→ τn } ` e : τ
Γ ` LAMBDA(hx1 , . . . , xn i, e) : τ1 × · · · × τn → τ
LET
Γ ` ei : τi , 1 ≤ i ≤ n
σi = generalize(τi , ftv(Γ)), 1 ≤ i ≤ n
Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ
Γ ` LET(hx1 , e1 , . . . , xn , en i, e) : τ
LETREC
Γ′ = Γ{x1 7→ τ1 , . . . , xn 7→ τn }
Γ′ ` ei : τi , 1 ≤ i ≤ n
σi = generalize(τi , ftv(Γ)), 1 ≤ i ≤ n
Γ{x1 7→ σ1 , . . . , xn 7→ σn } ` e : τ
Γ ` LETREC(hx1 , e1 , . . . , xn , en i, e) : τ
LETſTAR
Γ ` LET(hx1 , e1 i, LETſTAR(hx2 , e2 , . . . , xn , en i, e)) : τ n>0
Γ ` LETſTAR(hx1 , e1 , . . . , xn , en i, e) : τ
EMPTYLETſTAR BEGıN
EMPTYBEGıN
Γ`e:τ Γ ` ei : τi , 1 ≤ i ≤ n
Γ ` LETſTAR(hi, e) : τ Γ ` BEGıN(e1 , . . . , en ) : τn Γ ` BEGıN() : unit
VAL
′
Γ`e:τ σ = generalize(τ, ftv(Γ))
hd, Γi → Γ
hVAL(x, e), Γi → Γ{x 7→ σ}
VALREC EXP
Γ{x 7→ τ } ` e : τ σ = generalize(τ, ftv(Γ)) hVAL(it, e), Γi → Γ′
hVAL‐REC(x, e), Γi → Γ{x 7→ σ} hEXP(e), Γi → Γ′
DEFıNE
hVAL‐REC(f, LAMBDA(hx1 , . . . , xn i, e)), Γi → Γ′
hDEFıNE(f, (hx1 , . . . , xn i, e)), Γi → Γ′
VAL
C, Γ ` e : τ
θC is satisfied θΓ = Γ
σ = generalize(θτ, ftv(Γ))
hd, Γi → Γ′
hVAL(x, e), Γi → Γ{x 7→ σ}
(a) Add new primitives ref, !, and := with the same meanings as in full ML.
You will have to use ML “ref cells” to add a new form of value, and
(a) If a type error arises from a function application, show the function
and show the arguments. Show what types of arguments the function
expects and what the types the arguments actually have.
(b) If a type error arises from an ıF expression, show either that the type
of the condition is inconsistent with bool or that the two branches do
not have consistent types.
(c) Highlight the differences between inconsistent types. For example, if
your message says
function cons expected int * int list, got int * int,
this is better than saying “cannot unify int and int list,” but it is not
as good as showing which argument caused the problem, e.g.,
function cons expected int * >>int list<<, got int * >>int<<.
To associate a type error with a function application, look at the APPLY rule
on page 421 and see whether the constraint in the premise is solvable, and
if so, whether the constraint in the conclusion is solvable.
25. Elaboration into explicitly typed terms. Change the implementation of type
inference so that instead of inferring and checking types in one step, the
interpreter takes an untyped term and infers an explicitly typed term in
Typed µScheme. Adding information to the original code is an example of
elaboration. I recommend copying the syntax of Typed µScheme into a sub‐
module of your interpreter, as in
Chapters 1 to 7 don’t give us many ways to organize data. S‐expressions are great,
but you might have noticed that they serve as a kind of high‐level assembly language
on top of which you have to craft your own data structures. For programming at
scale, that’s not good enough—programmers need to define proper data structures
whose shapes and contents are known. Proper data‐definition mechanisms must
be able to express these possibilities:
All these possibilities can be expressed using algebraic data types. Algebraic data
types, supplemented by the base types, function types, and array types shown in
previous chapters, suffice to describe and typecheck representations of data at any
scale. They are ubiquitous in the ML family and in languages derived from it, in‐
cluding Standard ML, OCaml, Haskell, Agda, Coq/Gallina, and Idris.
Algebraic data types can be added to any language; this chapter adds them to
nano‐ML, making the new language µML. To add algebraic data types requires a
new species of value, a new expression form for looking at the values, and a new
definition form for introducing the types and values.
The new species of value is a constructed value. A constructed value is made
by applying some value constructor to zero or more other values. In the syntax,
however, zero‐argument value constructors aren’t applied; a zero‐argument value
constructor is a value all by itself. For example, '() is a value constructor for lists,
and it expects no arguments, so it is a constructed value. And cons is also a value
constructor for lists, but it expects arguments, so to make a constructed value, cons
must be applied to two other values: an element and a list.
A constructed value is interrogated, observed, or eliminated scrutinized by a
case expression. A case expression provides concise, readable syntax for asking a
457
key question about any datum: how was it formed, and from what parts? A case ex‐
pression gets the answer by using patterns: a pattern can match a particular value
constructor, and when it does, it can name each of the values to which the con‐
structor was applied. For example, the pattern (cons y ys) matches any cons cell,
8 and when it matches, it binds the name y to the car and ys to the cdr.
Case expressions and pattern matching eliminate the clutter associated with
functions like null?, car, cdr, fst, and snd. Instead of using such functions, you
Userdefined,
lay out the possible forms of the data, and for each form, you name the parts di‐
algebraic types (and
rectly. The resulting code is short and clear, and it operates at a higher level of
pattern matching)
abstraction than Scheme code or C code. With the right syntactic sugar, your code
458 can look a lot like algebraic laws (Section 8.4).
A case expression inspects a scrutinee, and it includes a sequence of choices, each
of which associates a pattern with a right‐hand side. And if the choices don’t cover
all possible cases, a compiler can tell you what case you left out (Exercise 38). As an
example, a case expression can be used to see if a list is empty; the scrutinee is the
formal parameter xs, and there are two choices: one for each form of list.
458. hpredefined µML functions 458i≡ 464d ▷
(define null? (xs) null? : (forall ['a] ((list 'a) ‑> bool))
(case xs
[(cons y ys) #t]
['() #f]))
Each choice has a pattern on the left and a result on the right.
Patterns are formed using value constructors, and each value constructor be‐
longs to a unique type. The type and its constructors are added to a basis (a type
environment and a value environment) by a new form of definition: the data defi
nition. In µML, this definition form comes in two flavors: “implicit” and “explicit.”
The implicit‑data form mimics Standard ML’s datatype form; it is simple and
almost impossible to get wrong. But implicit‑data is actually syntactic sugar for
µML’s explicit data form, which states, in full, the kind of the new type and the
types of its value constructors.
A data definition can name the new type whatever it wants. For example, a new
algebraic data type can be called int, in which case it hides the built‐in int. In the
interpreter, such type names are translated into internal types (Section 8.5).
Algebraic data types support new programming practices and also require new
theoretical techniques, both of which are addressed in this chapter.
• To know when two types are different, even when they have the same struc‐
ture, requires a theory of type generativity (Section 8.5). Algebraic data
types also require type theory and operational semantics that describe user‐
written type definitions (Section 8.7) and case expressions with pattern
matching (Section 8.8). The theory and semantics are implemented in my
interpreter.
Skills in both theory and implementation can be developed through the exercises
at the end of the chapter.
8.1 CAſE EXPREſſıONſ AND PATTERN MATCHıNG
To understand what you can do with algebraic data types, study the case expression,
which is described by this fragment of µML’s grammar:
exp ::= (case exp choice )
§8.1
choice ::= [pattern exp]
Case expressions
pattern ::= variablename and pattern
| valueconstructorname matching
| (valueconstructorname pattern ) 459
| _
The definition extends the basis with the information shown: the name and kind of
the new type, traffic‑light, plus the name and type of each new value construc‐
tor. These value constructors take no arguments, so the only values of the new type
are RED, GREEN, and YELLOW.
Using a case expression, I can define a function to change a light.
459b. htranscript 459ai+≡ ◁ 459a 460a ▷
change‑light : (traffic‑light ‑> traffic‑light)
‑> (define change‑light (light)
(case light
[GREEN YELLOW]
[YELLOW RED]
[RED GREEN]))
‑> (change‑light GREEN)
YELLOW : traffic‑light
8 [RED
[GREEN
'stop]
'go]
[YELLOW 'go‑faster]))
Userdefined, ‑> (light‑meaning GREEN)
algebraic types (and go : sym
pattern matching) Again, light evaluates to GREEN. The first choice has pattern RED, which doesn’t
match GREEN. The second choice has pattern GREEN, which does match GREEN, and
460
the result is the right‐hand side: green means 'go.
Multiple constructed values can be inspected with multiple case expressions.
As an example, two traffic lights can be compared to see if one is safer to drive
through. To compute that GREEN is safer than YELLOW, I use nested case expressions:
460b. htranscript 459ai+≡ ◁ 460a 460c ▷
safer? : (traffic‑light traffic‑light ‑> bool)
‑> (define safer? (light1 light2)
(case light1
[GREEN (case light2
[GREEN #f]
[YELLOW #t]
[RED #t])]
[YELLOW (case light2
[GREEN #f]
[YELLOW #f]
[RED #t])]
[RED #f]))
safer? : (traffic‑light traffic‑light ‑> bool)
‑> (safer? GREEN YELLOW)
#t : bool
In this example, light1 evaluates to GREEN, and the first pattern matches. On the
right‐hand side, the inner case first evaluates light2 to get YELLOW, then finds that
the pattern in the second choice matches, and finally returns the right‐hand side #t.
Nested patterns
Nested case expressions can be ugly and hard to read; they don’t make it obvious
what is being compared. A more idiomatic comparison forms a pair and scruti‐
nizes it using nested patterns. In a nested pattern, a value constructor is applied to
one or more patterns that are also formed using value constructors. In this exam‐
ple, I nest value constructors for traffic‑light inside the predefined PAIR value
constructor, as in the nested pattern (PAIR GREEN YELLOW).
460c. htranscript 459ai+≡ ◁ 460b 461a ▷
‑> (define safer? (light1 light2)
(case (PAIR light1 light2)
[(PAIR GREEN GREEN) #f]
[(PAIR GREEN YELLOW) #t]
[(PAIR GREEN RED) #t]
[(PAIR YELLOW GREEN) #f]
[(PAIR YELLOW YELLOW) #f]
[(PAIR YELLOW RED) #t]
[(PAIR RED GREEN) #f]
[(PAIR RED YELLOW) #f]
[(PAIR RED RED) #f]))
safer? : (traffic‑light traffic‑light ‑> bool)
This time the expression (PAIR light1 light2) is evaluated, and it produces the
value (PAIR GREEN YELLOW). When this value is scrutinized in the case expression,
the second choice matches it, and the result is the right‐hand side #t.
461a. htranscript 459ai+≡ ◁ 460c 461b ▷
‑> (safer? GREEN YELLOW)
#t : bool
§8.1
Case expressions
Patterns with wildcards and pattern
matching
Nested patterns make it easy to write out an entire truth table, but an entire truth
table can get big. To write fewer patterns, I use the “wildcard” pattern, written with 461
a single underscore, which means “match anything; I don’t care.” To test safety at
traffic lights, “I don’t care” can be used with every color:
• Green is no safer than itself but is safer than anything else—I don’t care what.
• Yellow is safer than red but not safer than anything else—I don’t care what.
• Red is not safer than anything—I don’t care what.
From here, let’s assume the lights are green, and let’s ask how fast we can drive.
If, like me, you drive between the U.S. and Canada, it’s not always easy to know— GREEN 459a
my countrymen can’t agree with the Canadians on what constitutes a speed. PAIR B
RED 459a
• In the U.S., a speed is (MPH n), where n is a number. type traffic‑
light 459a
• In Canada, a speed is (KPH n), where n is a number. YELLOW 459a
This sad situation can be expressed by defining an algebraic data type. Unlike RED,
GREEN, and YELLOW, MPH and KPH are value constructors that take arguments:
461c. htranscript 459ai+≡ ◁ 461b 462a ▷
‑> (implicit‑data speed
[MPH of int]
[KPH of int])
speed :: *
MPH : (int ‑> speed)
KPH : (int ‑> speed)
pattern matching) [(PAIR (MPH n1) (KPH n2)) (< (* 5 n1) (* 8 n2))]
[(PAIR (KPH n1) (MPH n2)) (< (* 8 n1) (* 5 n2))]))
462 ‑> (define faster (speed1 speed2)
(if (speed< speed1 speed2) speed2 speed1))
‑> (faster (KPH 50) (MPH 30))
(KPH 50) : speed
What really matters is whether I’m obeying the speed limit. In Quebec and New
England, a speed limit is just a speed. But in parts of western North America and
Europe, there is no speed limit. So a speed limit could be represented as one of the
following:
• (SOME s), where the posted speed limit is a speed s
• NONE, where there is no posted speed limit
462b. htranscript 459ai+≡ ◁ 462a 462c ▷
‑> (define legal‑speed? legal‑speed? : (speed (option speed) ‑> bool)
(my‑speed limit)
(case limit
[(SOME max) (not (speed< max my‑speed))]
[NONE #t]))
Can I go 65 mph in a 110 kph zone? On the autobahn, if there is no speed limit?
462c. htranscript 459ai+≡ ◁ 462b 462d ▷
‑> (legal‑speed? (MPH 65) (SOME (KPH 110)))
#t : bool
‑> (legal‑speed? (MPH 65) NONE)
#t : bool
Value constructors SOME and NONE have many more uses. For example, they can
describe the result of looking up a key k in an association list:
• (SOME v ), if key k is associated with value v
• NONE, if key k is not present
They can also describe the result of reading a line from a file:
• (SOME s), where s is a string representing the next line from the file
• NONE, if the end of the file has been reached
If SOME can be used with a speed, or a string, or a value of unknown type, what
type must it have? A polymorphic type. Value constructors SOME and NONE belong
to µML’s built‐in option type, which is copied from full Standard ML.
462d. htranscript 459ai+≡ ◁ 462c 463c ▷
‑> SOME
<function> : (forall ['a] ('a ‑> (option 'a)))
‑> NONE
NONE : (forall ['a] (option 'a))
The option type is not primitive; it is predefined using ordinary user code.
Its definition uses the explicit data form, which gives the kind of the option type
constructor and the full types of the SOME and NONE value constructors:
463a. hpredefined µML types 463ai≡ 473a ▷
(data (* => *) option
[SOME : (forall ['a] ('a ‑> (option 'a)))]
§8.1
[NONE : (forall ['a] (option 'a))])
Case expressions
If you prefer less notation, any algebraic data type in µML can also be defined using
and pattern
implicit‑data:
matching
463b. htranscript of implicit definitions 463bi≡
‑> (implicit‑data ['a] option 463
NONE
[SOME of 'a])
option@{2} :: (* => *)
NONE : (forall ['a] (option@{2} 'a))
SOME : (forall ['a] ('a ‑> (option@{2} 'a)))
But what is option@{2}?! It is the print name of the option type just defined. The
suffix “@{2}” lets you know that this type is a redefinition of the option type built
into µML. Types option and option@{2} are incompatible; a function that works
with one won’t work with the other. This incompatibility is a consequence of the
generativity of datatype definitions (Section 8.5).
No matter how NONE and SOME are defined, they are polymorphic. And if they
are used in a function, that function may also be polymorphic. As an example,
function get‑opt takes a value of type (option 'a) and a default value of type 'a,
and it returns either the option value or the default:
463c. htranscript 459ai+≡ ◁ 462d 463d ▷
get‑opt : (forall ['a] ((option 'a) 'a ‑> 'a))
‑> (define get‑opt (maybe default)
(case maybe
[(SOME a) a]
[NONE default]))
get‑opt : (forall ['a] ((option 'a) 'a ‑> 'a))
In a datatype definition, a value constructor can take an argument of the type being
defined—which makes the data type recursive. The best simple example is a list;
another good, simple example is a binary tree. At each internal node, a simple
append B
binary tree might carry one value of type 'a, plus left and right subtrees: KPH 461c
463d. htranscript 459ai+≡ ◁ 463c 463e ▷ MPH 461c
‑> (data (* => *) bt NONE B
[BTNODE : (forall ['a] ('a (bt 'a) (bt 'a) ‑> (bt 'a)))] PAIR B
SOME B
[BTEMPTY : (forall ['a] (bt 'a))])
type speed 461c
bt :: (* => *)
BTNODE : (forall ['a] ('a (bt 'a) (bt 'a) ‑> (bt 'a)))
BTEMPTY : (forall ['a] (bt 'a))
Such a tree’s elements can be listed in preorder (node before children):
463e. htranscript 459ai+≡ ◁ 463d 464a ▷
preorder‑elems : (forall ['a] ((bt 'a) ‑> (list 'a)))
‑> (define preorder‑elems (t)
(case t
[BTEMPTY '()]
[(BTNODE a left right)
(cons a (append (preorder‑elems left) (preorder‑elems right)))]))
And a test case:
464a. htranscript 459ai+≡ ◁ 463e 464b ▷
‑> (define single‑node (a) single‑node : (forall ['a] ('a ‑> (bt 'a)))
Most often, an algebraic data type defines more than one way to form a constructed
value—that is, more than one value constructor. But an algebraic datatype can use‐
fully have just a single constructor, as shown by the pair type:
464d. hpredefined µML functions 458i+≡ ◁ 458 464e ▷
(data (* * => *) pair
[PAIR : (forall ['a 'b] ('a 'b ‑> (pair 'a 'b)))])
Using this definition, functions pair, fst, and snd don’t have to be implemented as
primitives the way they do in nano‐ML; they are defined using ordinary user code:
464e. hpredefined µML functions 458i+≡ ◁ 464d 474c ▷
(val pair PAIR)
(define fst (p)
(case p [(PAIR x _) x]))
(define snd (p)
(case p [(PAIR _ y) y]))
(Because pattern matching is more idiomatic, functions fst and snd are rarely
used, but they are included for compatibility with nano‐ML.)
Single‐constructor types like pair are typically used in two different ways:
• When the single constructor takes multiple arguments, like PAIR, the alge‐
braic data type acts as a record type. This usage can be supported by syntactic
sugar; µML provides a record definition form, which desugars into datatype §8.1
definition and a sequence of function definitions. Case expressions
and pattern
• When the single constructor takes a single argument, the algebraic data type matching
acts as a renaming of some other type. The types that are most commonly
renamed are types that are used with many meanings, like integers and 465
Booleans; for example, in order to distinguish a height from a weight, we
might rename int:
465. htranscript 459ai+≡ ◁ 464c 470b ▷
‑> (data * height [HEIGHT : (int ‑> height)])
‑> (data * weight [WEIGHT : (int ‑> weight)])
‑> (val h (HEIGHT 196))
(HEIGHT 196) : height
‑> (val w (WEIGHT 87))
(WEIGHT 87) : weight
In full ML, this kind of renaming has zero run‐time cost; at run time,
(HEIGHT 196) is represented in exactly the same way as just 196.
Algebraic data types can express all the possibilities we demand from a “proper”
type‐definition mechanism. These possibilities can be expressed in the language
of type theory:
• A type whose values can take on multiple forms is called a sum type.
• A type that gathers multiple parts is called a product type.1
• A type with a part that is like the whole is called a recursive type.
At first glance, an algebraic data type appears to be just a sum type: each form
corresponds to a value constructor. But because each value constructor may carry
any number of parts, an algebraic data type is a sum of products. And it expresses
the others as special cases: Omit carried values, as in traffic‑light, and it’s a
pure sum. Or define a type with just one value constructor, as in pair, and it’s a
append B
pure product.
type bt 463d
Because algebraic data types can express sums, products, and recursion, they BTEMPTY 463d
are technically universal. But they are also universal in a more important, less BTNODE 463d
technical sense: sums of products turn out to be a great model for data structures. preorder‑elems
463e
For example, a linked list is a sum of products. So is a binary tree. So are the
abstract‐syntax trees found throughout this book. A sum of products is something
that every programmer must know how to code, in any language. As examples,
I discuss two languages or language families: C, the language of Chapters 1 to 4
(and also of the world’s computing infrastructure), and object‐oriented languages,
a popular and maybe even dominant language family for over 20 years.
In C, a general sum of products is awkward to code. C’s union type implements
a sum, but a value of union type doesn’t record which choice is represented—that
1
If you are wondering about the names, pick some finite types like bool and traffic‑light, then
count the number of values that inhabit a sum like “Boolean or light” or a product like “Boolean and
light.” And note that “product” is the Cartesian product, which you may have studied in math class.
information has to be stored in a tag off to the side. In the general case, a sum of
products is therefore a struct containing both a tag and a union of structs. But there
is an important and common special case: a sum with only two forms, only one of
which carries a product. In that case, the sum of products can be represented by a
8 pointer:
466 This representation works nicely for lists, binary trees, and things like option.
In particular, it supports a simple, efficient test to identify the form of the data:
ask if a pointer is NULL.
In object‐oriented languages (Chapter 10), a sum of products emerges when‐
ever different forms of objects work together to implement the same abstraction.
Each individual object is a product: the product of the values of all its instance vari
ables (sometimes called members or fields). And a collection of related objects is
a sum of those products, referred to by the conventions of a particular language.
For example, in Smalltalk, that collection would be a set of objects that respond to
the same protocol; in Java, a set of objects that implement the same interface; and
in C++, a set of objects whose classes inherit from the same superclass.
With these universal ideas in mind, we’re ready to study algebraic data types
in the context of a complete programming language: µML. A programmer’s view
encompasses an informal description of type definitions, case expressions, and
pattern matching, with examples; equational reasoning with algebraic data types,
and the extension of pattern matching to other language constructs. A theorist’s
view encompasses generativity and its effect on type equivalence; a theory of user‐
defined types; and a theory of case expressions.
Algebraic data types combine nicely with nano‐ML; the resulting language is
called µML. µML’s concrete syntax is shown in Figure 8.1 on the facing page. The
syntax includes not only the case‐expression and data‐definition forms, but also
three syntactic categories not found in nano‐ML: patterns, kinds, and type expres
sions.
• Types and kinds are used data definitions. The only truly new definition form
is data; the implicit‑data and record forms are syntactic sugar for data.
Unlike the definition and expression forms in nano‐ML, which do not men‐
tion kinds or types, the data definition specifies both kind and typeexp, using
the same syntax as Typed µScheme.
• A variable x matches any value v , and the match produces the singleton set
of bindings {x 7→ v}.
2
Compilers for languages that use algebraic data types, including ML and Haskell, don’t actually try
each pattern in sequence. Instead, they translate each case expression into an efficient automaton.
Using such an automaton, the matching pattern is found cheaply; the cost is typically a few machine
instructions times the maximum number of value constructors appearing in any one pattern.
Table 8.2: Examples of pattern matching
As implemented in Standard ML, pattern matching has a pitfall: if you misspell the
name of a value constructor, which matches only itself, Standard ML might take
the misspelling for a value variable, which matches anything. If you’re lucky, you’ll
get a warning from the compiler, but whether you are warned or not, you’ll get
different behavior from what you intended. For example, given a list of lights, does
this Standard ML code test to see if the first one is green?
469. hpitfall in Standard ML 469i≡
datatype traffic_light = GREEN | YELLOW | RED
val startsWithGreen : traffic_light list ‑> bool =
fn lights =>
case lights
of GREEEN :: _ => true
| _ => false
As shown on the next page, this code doesn’t do what you might think.
It seems that any list starts with GREEN:
470a. htranscript of Standard ML using startsWithGreen 470ai≡
‑ startsWithGreen [GREEN, YELLOW, RED];
val it = true : bool
Userdefined, In the case expression on the previous page, I misspelled GREEN. The compiler
algebraic types (and parses GREEEN as a variable, so the first choice matches any nonempty list. And the
pattern matching) compiler gives no warning, because the second case can also match: it matches the
empty list. What’s called startsWithGreen is actually a “not null” function.
470 This problem can be mitigated by using the wildcard pattern cautiously—for ex‐
ample, only as an argument to a value constructor. But the problem can be avoided
entirely through better language design. Languages like OCaml, Haskell, and µML
mandate a spelling convention: depending on the way a name is spelled, it can
stand for a variable or for a value constructor, but never both. (Standard ML uses
such a convention in the type language, but not in the term language.) µML’s con‐
vention, in order to preserve compatibility with nano‐ML and µScheme, is more
elaborate than most:
• If a name contains a dot (.), every character up to and including the last dot is
removed. If what’s left begins with a capital letter or with the # symbol, it’s the
name of a value constructor; it cannot stand for a variable or function.
• To enable us to write lists that look like µScheme and nano‐ML lists, the name
cons and the syntax '() are both deemed to be value constructors.
• For compatibility with the record form, any name beginning with make‑ is
the name of a value constructor.
• Any other name is the name of a value variable, or simply a variable; it cannot
stand for a value constructor.
In µML, the name GREEEN begins with a capital letter, so it must be the name of a
value constructor. Since no value constructor by that name appears in the environ‐
ment, the following starts‑with‑green function, which has the same bug as the
version above, is rejected by the type checker:
470b. htranscript 459ai+≡ ◁ 465 470c ▷
‑> (val starts‑with‑green
(lambda (lights)
(case lights
[(cons GREEEN _) #t]
[_ #f])))
type error: no value constructor named GREEEN
In µML, as in Haskell, an algebraic data type may be defined in two ways. The
§8.2
data form gives the kind of the type constructor and the type of each of its value
Algebraic data
constructors. The implicit‑data form leaves the kind of the type constructor to
types in µML
be inferred, and it gives only the argument types, if any, of each value constructor.
Each form has advantages and disadvantages: 471
• In the data form, all types and kinds appear explicitly in the source code, so
they are crystal clear. But to use the data form, you have to understand and
follow the rules given below for the types of value constructors; if you break
the rules, your definition will be rejected by the type checker.
• In the implicit‑data form, you have to follow only one rule: the argument
type of each value constructor must be well kinded. But the types of the value
constructors don’t appear in the source code; if you want to know them, you
have to reconstruct them mentally.
The data form supports some fancy extensions that are described in Appendix S,
but the implicit‑data form is easier to write, and it is isomorphic to the corre‐
sponding forms in Standard ML and OCaml. (Full Haskell supports both forms,
including extensions; OCaml supports extensions by adding constraints to the
implicit‑data form.)
A data form that defines algebraic data type T looks like this:
• Every ti must be well kinded. Each type ti may use type name T , but only in
a way that is consistent with its kind κ: If κ is *, then T is a type all by itself,
and it takes no parameters. If κ has the form (* · · · * => *), then T takes as
many type parameters as there are stars to the left of the arrow.
• If T takes type parameters, then each value constructor must be polymor‐ cons B
phic, and in its type ti , the number of variables under the forall must be GREEN 459a
equal the to number of type parameters T is expecting. If T takes no type list3 B
type traffic‑
parameters, each value constructor must be monomorphic. light 459a
For example, the traffic‑light type takes no parameters, and its value con‐
structors are monomorphic. But the option type takes a type parameter, so
its value constructors SOME and NONE are polymorphic.
• Underneath the forall, if any, each type ti has a result type, and that type
must be valid. The result type is determined by the shape of the type under
the forall: if it is a function type, the result type is the type to the right of
the function arrow. Otherwise, the result type is whatever appears under
the forall. Any given ti may or may not use forall, and it may or may not
be a function type, so there are four cases in total, as illustrated by these
examples:
These rules make type inference relatively straightforward. In more ambitious lan‐
guages, some of the rules can be relaxed—full languages like Haskell support not
only standard algebraic data types but also generalized algebraic data types and ex
istential algebraic data types, as described in Appendix S—but relaxing the rules
makes type inference more difficult.
The rules are checked when a data definition is typed. If the rules are re‐
spected, type T is added to the basis, where it stands for a unique type constructor
of kind κ, distinct from all other type constructors. The guarantee of distinction
makes the definition generative. Each Ki is also added to the basis, to the type en‐
vironment with type ti , and to the value environment either as the bare constructed
value Ki or as an anonymous primitive function that applies Ki to its arguments.
The implicit‑data form is described by this EBNF:
def ::= (implicit‑data
[ 'typevariablename ] typeconstructorname
valueconstructorname [valueconstructorname of typeexp ] )
The form includes optional type parameters, the name of the type constructor be‐
ing defined, and specifications of one or more value constructors. A value con‐
structor that takes no arguments is specified by its name. A value constructor that
takes arguments is specified by its name, followed by the keyword of, followed by
the types of its arguments, all wrapped in parentheses. Using metavariables, an
example of the form looks like this:
(implicit‑data [α1 · · · αm ] T
K1 · · · Kk [Kk+1 of t · · · t] · · · [Kn of t · · · t])
This implicit‑data form requires less notation than the data form—for example,
there is never an explicit forall—and it imposes just one rule: the t’s must be well
kinded. The implicit‑data form closely resembles the datatype form in Stan‐
dard ML, which is used to implement the interpreters in this book from Chapter 5
onwards.
When typed and evaluated, implicit‑data does exactly what data does: it adds
T to the basis, bound to a fresh type constructor, and it adds each Ki . Each Ki ’s
result type tr is either T alone, if there are no type parameters, or if there are type
parameters, it is (T α1 · · · αm ). If Ki is specified just by its name, its type is the
result type tr ; if Ki is specified using the form [Ki of t1 · · · tn ], then its type
(implicit‑data T (data * T
K1 [K1 : T]
. .
. .
. △ .
[Ki of t1 · · · tn ] = [Ki : (t1 · · · tn ‑> T )]
. .
. .
. .
§8.2
) )
Algebraic data
types in µML
(a) Without type parameters
473
(data (*1 ··· T
*m => *)
(implicit‑data (α1 · · · αm ) T [K1 : (forall (α1 · · · αm )
K1 (T α1 · · · αm ))]
. .
. .
. .
[Ki of t1 · · · tn ] △
= [Ki : (forall (α1 · · · αm )
(t1 · · · tn ‑>
.
. (T α1 · · · αm )))]
.
.
.
) .
)
Many of the algebraic data types found in Standard ML are also predefined in µML.
They are defined using data or implicit‑data. In most cases, to make the types
of the value constructors explicit, I use the data form.
A Boolean is either #t or #f.
473a. hpredefined µML types 463ai+≡ ◁ 463a 473b ▷
(data * bool
[#t : bool]
[#f : bool])
8 Type pair is defined on page 464, with accompanying functions pair, fst, and
snd. Types for triples and larger tuples are also predefined; the value constructor
Userdefined, for type triple is TRIPLE, and the value constructors for types 4‑tuple through
algebraic types (and 10‑tuple are T4 to T10, respectively (Appendix S). These types are defined with‐
pattern matching) out any accompanying functions. If you want functions, define them using pattern
matching. Or do Exercise 27 and match tuples directly in let forms.
474 The order type is a standard result type for comparison functions. It represents
a relation between elements of a totally ordered set: one element may be less than,
equal to, or greater than another.
474b. hpredefined µML types 463ai+≡ ◁ 474a
(implicit‑data order LESS EQUAL GREATER)
In µScheme and nano‐ML, list values are built in, so the basic list functions must
be built in as primitives. In µML, the basic list functions are user‐defined.
475a. hpredefined µML functions 458i+≡ ◁ 474c 475b ▷
null? : (forall ['a] ((list 'a) ‑> bool))
(define null? (xs) car : (forall ['a] ((list 'a) ‑> 'a))
§8.2
(case xs ['() #t] cdr : (forall ['a] ((list 'a) ‑> (list 'a)))
Algebraic data
[(cons _ _) #f]))
types in µML
(define car (xs)
(case xs ['() (error 'car‑of‑empty‑list)] 475
[(cons y _) y]))
(define cdr (xs)
(case xs ['() (error 'cdr‑of‑empty‑list)]
[(cons _ ys) ys]))
Don’t get fond of null?, car, and cdr. These functions are rarely needed; most code
uses pattern matching, as in these versions of append and revapp:
475b. hpredefined µML functions 458i+≡ ◁ 475a 475c ▷
append : (forall ['a] ((list 'a) (list 'a) ‑> (list 'a)))
revapp : (forall ['a] ((list 'a) (list 'a) ‑> (list 'a)))
Function bind operates on association lists. The code uses only pattern match‐
ing, not null?, car, or cdr. I encourage you to compare it with the nano‐ML version
in chunk S423b.
475c. hpredefined µML functions 458i+≡ ◁ 475b 475d ▷
bind : (forall ['a 'b] ('a 'b (list (pair 'a 'b)) ‑> (list (pair 'a 'b))))
(define list1 (x) (cons x '()))
(define bind (x y alist)
(case alist
['() (list1 (pair x y))]
[(cons p ps)
(if (= x (fst p)) EQUAL B
(cons (pair x y) ps) GREATER B
(cons p (bind x y ps)))])) LESS B
type speed 461c
Functions find and bound? also improve on their nano‐ML versions. When a speed< 462a
key is not found, find needn’t call error; it can instead return a value of option
type. Using nested patterns, find can be implemented without using fst or snd.
475d. hpredefined µML functions 458i+≡ ◁ 475c 476 ▷
find : (forall ['a 'b] ('a (list (pair 'a 'b)) ‑> (option 'b)))
(define find (x alist)
(case alist
['() NONE]
[(cons (PAIR key value) pairs)
(if (= x key)
(SOME value)
(find x pairs))]))
In nano‐ML, bound? must reimplement the same search algorithm used in
find. But in µML, bound? simply calls find.
476. hpredefined µML functions 458i+≡ ◁ 475d
bound? : (forall ['a 'b] ('a (list (pair 'a 'b)) ‑> bool))
8 (define bound? (x alist)
(case (find x alist)
Userdefined, [(SOME _) #t]
[NONE #f]))
algebraic types (and
pattern matching) The rest of the predefined functions are defined in Section S.2.4 (page S445).
476
8.3 EQUATıONAL REAſONıNG WıTH CAſE EXPREſſıONſ
Like µScheme and nano‐ML, µML is a functional language. As such, it supports the
same equational‐reasoning techniques as µScheme, and even the same algebraic
laws (Section 2.5). But unlike µScheme, µMLhas algebraic data types and case ex‐
pressions. And equational reasoning can be used on them, too.
The laws for case expressions follow from the rules for evaluating case expressions:
• If its scrutinee matches the pattern in the first choice, a case expression is
equivalent to a let expression.
• If its scrutinee doesn’t match the pattern in the first choice, a case expression
is equivalent to the same case expression with the first choice removed.
If you can’t tell whether a scrutinee matches the pattern in a first choice, then you
break your proof down by cases, adding one case for each value constructor in some
algebraic data type. You can make it easy to tell by eliminating nested patterns.
Matching of non‐nested patterns is described by simple laws.
Each law of matching applies to a case expression in which the first choice con‐
tains one particular form of pattern: variable, bare constructor, or constructor ap‐
plication. If its first pattern is a variable, a case expression is equivalent to a let
expression.
If its first pattern is a value constructor, and if the scrutinee is that same value con‐
structor, then a case expression is equivalent to the first right‐hand side.
(case K [K e′ ] · · · ) = e′
If the first pattern applies a value constructor K , and if the scrutinee is constructed
by applying K , then a case expression is equivalent to a let expression.
(case (K e1 · · · en )
= (let ([x1 e1 ] · · · [xn en ]) e′ )
[(K x1 · · · xn ) e ′ ] · · · )
If the first pattern involves a value constructor, it might not match the scrutinee.
In that event, the case expression is equivalent to a similar expression with the
first choice removed. For example, when K = 6 K ′,
(case (K e1 · · · en )
(case (K e1 · · · en )
[(K ′ x1 · · · xm ) e′ ] =
′′ [p e′′ ] · · · ). §8.3
[p e ] · · · )
Equational
Other laws you can work out for yourself. reasoning with
To show some case‐expression laws at work, I prove two properties of the case expressions
bt‑map function, which is defined on the binary trees of Section 8.1 (page 463):
477
Both properties are proved by appealing to the definition of bt‑map; the proofs are
shown in Figure 8.4.
The first proof uses just the one case‐expression law; the second proof uses two
case‐expression laws, plus laws that simplify let expressions.
As these examples show, the laws for case expressions enable us to prove equal‐
ities involving particular constructed values. But to prove properties that hold for
all values of a given type τ , we need a proof principle for τ . When τ is an algebraic
data type, the proof principle requires only that we prove one case for each of τ ’s
value constructors. And if any value constructor takes an argument of type τ , that
case can use an inductive hypothesis.
As an example, I prove this “map‐preorder” property:
bt‑map f
(bt τ ) (bt τ ′ )
preorder‑elems preorder‑elems
map f
(list τ ) (list τ ′ )
Officially, µML’s patterns appear only in case expressions. But in real languages,
patterns aren’t just for case expressions; patterns are used anywhere variables are
8 bound, including lambda, let, let*, define, and val. In µML, thanks to syn‐
tactic sugar, patterns can appear in all these places as well. Such patterns are
rewritten into case expressions; the rewrite rules, which you can implement (Ex‐
Userdefined,
ercises 23 to 28), are described below.
algebraic types (and
A pattern may appear as the single argument to a lambda expression. Such a
pattern matching)
pattern is most useful when the argument is a pair or a triple or something else
480 with just one value constructor:
480a. hpatternseverywhere transcript 480ai≡ 480b ▷
‑> (lambda ((PAIR x _)) x)
<function> : (forall ['a 'b] ((pair 'a 'b) ‑> 'a))
‑> (lambda ((PAIR _ y)) y)
<function> : (forall ['a 'b] ((pair 'a 'b) ‑> 'b))
As another example, same‑length, which compares the lengths of two lists, uses
more interesting patterns:
481c. hpatternseverywhere transcript 480ai+≡ ◁ 481b 481d ▷
‑> (define* same‑length? : (forall ['a 'b] ((list 'a) (list 'b) ‑> bool))
All these definitions are desugared using the same rule. A clausal definition
defines function f using k clauses, and in each clause, f is applied to n patterns:
(define* (define f · · · xn )
(x1
[(f p1,1 · · · p1,n ) e1 ] (case (Tn x 1 · · · xn )
[(f p2,1 · · · p2,n ) e2 ] [(Tn p1,1 · · · p1,n ) e1 ]
△
.
.
= [(Tn p2,1 · · · p2,n ) e2 ]
. '() B
.
.
[(f pk,1 · · · pk,n ) ek ]) . cons B
[(Tn pk,1 · · · pk,n ) ek ])), GREEN 459a
NONE B
PAIR B
pair B
where none of x1 , . . . , xn appears free in any expression ei . RED 459a
An anonymous function can be defined clausally using lambda*; as an example, reverse B
I define a function that splits a list into two halves (Section E.2). It uses an internal SOME B
function (scan ˆ l r ys ), which, when possible, transfers an element from r to ˆl and type speed 461c
speed< 462a
drops two elements from ys , then continues. When ys or r is exhausted, scan type traffic‑
returns the pair (l, r). A letrec with a clausal lambda* is much simpler than nested light 459a
case expressions (chunk S130a): YELLOW 459a
This expansion does evaluate e exactly once, and it does bind each yi with the cor‐
rect, generalized type scheme. But it clobbers the variable it.
483b. hpatternseverywhere transcript 480ai+≡ ◁ 483a
‑> (val (PAIR left right) (halves '(a b c d)))
(PAIR (a b) (c d)) : (pair (list sym) (list sym))
(c d) : (list sym)
(a b) : (list sym)
‑> left
(a b) : (list sym)
‑> right
(c d) : (list sym)
The syntactic sugar completes what you need to know in order to program ef‐
fectively using algebraic data types. To understand more deeply how they work,
consult the theory and code in the next sections: when and how a user‐defined
type is distinct from similar types (Section 8.5), how the relevant syntax and values
are represented (Section 8.6), how type definitions are typed and evaluated (Sec‐
tion 8.7), and how case expressions are typechecked and evaluated (Section 8.8).
In any language that has user‐defined types, a programmer has to know if and when
the types they define are equivalent to anything else. That knowledge is determined
by the ways the language uses three concepts: structural equivalence, generativity, halves 481d
PAIR B
and type abbreviation. pair B
• Structural equivalence says two types are equivalent when they are applica‐
tions of equivalent type constructors to equivalent type arguments, as in
Typed µScheme’s EQUıVAPPLıCATıONſ rule (page 369):
τi ≡ τi′ , 1 ≤ i ≤ n τ ≡ τ′ .
(EQUıVAPPLıCATıONſ)
(τ1 , . . . , τn ) τ ≡ (τ1′ , . . . , τn′ ) τ ′
In µML, as in Typed µScheme and nano‐ML, structural equivalence is used
for list types, function types, pair types, and so on. In C, structural equiv‐
alence is used for pointer types and array types; for example, pointers to
equivalent types are equivalent. And in Modula‐3, for example, structural
equivalence is used for record types: record types with the same fields are
equivalent, provided that corresponding fields have equivalent types.
By design, only structural equivalence and generativity are used in µML; type ab‐
breviations are added in Exercise 32.
Generativity is a powerful idea, but in older work, especially work oriented to‐
ward compilers, you may see the term “name equivalence” or “occurrence equiv‐
alence.” These terms refer to special applications of generativity—for example,
“name equivalence” may describe a language that has a syntactic form which re‐
sembles a type abbreviation but is generative. The terms “name equivalence” and
“occurrence equivalence” usually describe language designs that were popular in
the 1970s, but these terms are outmoded and should no longer be used. The con‐
cept of generativity is more flexible and can be applied to more designs.
Of what use is generativity? Generativity helps ensure that two types are equiv‐
alent only when you mean them to be equivalent. This issue matters most when
programs are split into multiple modules (Chapter 9). For example, if two record
types both have numeric fields heading and distance, but one is degrees and miles
and the other is radians and kilometers, you want them not to be equivalent.
Generativity should inform our thinking about design, theory, and implemen‐
tation of programming languages.
• When definitions can be generative and type names can be redefined, a sin‐
gle name can stand for different types in different parts of a program. In this
way, a type name is like a variable name, which can stand for different values
in different parts of a program.
In the interpreter, types are represented in two ways. Type syntax, repre‐
sented by ML type tyex (chunk S454c) and shown mathematically as t, ap‐
pears in programs, and type syntax is built up using type names (ML type
name). Types themselves, represented by ML types ty and type_scheme
(chunk 408) and shown mathematically as τ and σ , are used by the type
checker, and types are built up using type constructors (ML type tycon).
The internal representation of type constructors, the generation of fresh type con‐ §8.6
structors, and a type‐equivalence function are presented below. Abstract syntax
and values of µML
8.5.1 Representing and generating type constructors 485
The representation of a type constructor must solve two problems: it should be easy
to create a type constructor that is distinct from all others, and it should be easy to
tell if two type constructors are the same. I address both problems by assigning
each type constructor an identity, which I represent by an integer.
485a. hfoundational definitions for generated type constructors 485ai≡ (S438d) 485b ▷
type tycon_identity = int
Integers are great for algorithms but not so good for talking to programmers.
To make it possible to print an informative representation of any type, I represent
a type constructor as a record containing not only its identity but also a name used
to print it.
485b. hfoundational definitions for generated type constructors 485ai+≡ (S438d) ◁ 485a 485c ▷
type tycon = { printName : name, identity : tycon_identity }
User‐defined, algebraic data types require new syntactic forms, which are shown
in Figure 8.5:
486d. hdefinitions of exp and value for µML 486bi+≡ (S437b) ◁ 486b
and value
= CONVAL of vcon * value list
| SYM of name
| NUM of int
| CLOSURE of lambda * value env ref
| PRIMITIVE of primop
withtype lambda = name list * exp
and primop = value list ‑> value
source code—and they play different roles in patterns. They are represented
differently in expressions, as well; that way, if a name is not found, the
error message can clarify what the interpreter was expecting, as shown in
chunk S452c.)
New type constructors are created by type definitions. And in any language with
user‐defined types, type definitions introduce new names, which have to be ac‐
3
Nano‐ML’s internal language could be simplified even more by eliminating the IFX syntax; if could
be syntactic sugar for case. But to enable µML’s interpreter to share code with nano‐ML’s interpreter,
I have kept both forms.
counted for in the type theory. And if type constructors and type names are dis‐
tinct, type constructors must also be accounted for. In µML’s type theory, the ac‐
counting works like this:
• Each type name and type variable has a kind and stands for a type. The kind
and the type are associated with the name in a kind environment ∆, which
is part of µML’s basis. ∆ is extended by each data definition, which adds a §8.7
binding for the name of the defined type. Theory and
implementation of
• µML’s basis also includes a typeconstructor set M , which contains the set of userdefined types
all type constructors ever created. A type constructor created by a data def‐
487
inition must not be a member of M ; that requirement makes the data form
generative. Once created, each new type constructor is added to M . In the
code, set M is represented by the mutable variable nextIdentity, which is
used by freshTycon to ensure that each type constructor has a unique iden‐
tity (chunk S449b). M is the set given by this equation:
M = {i | 0 ≤ i < !nextIdentity}.
In µML’s data form, the type of every value constructor is explicit in the syntax. type env 304
type kind 355a
The type of each value constructor must be compatible with the new type that is type name 303
being defined; for example, a value constructor of type int is not compatible with type tyex S454c
a definition of type bool. Compatibility is enforced by the σi ≼ µ :: κ premises in
this rule:
µ∈ /M M ′ = M ∪ {µ}
′
∆ = ∆{T 7→ (µ, κ)}
∆′ ` ti ; σi :: ∗, 1 ≤ i ≤ n
σi ≼ µ :: κ, 1 ≤ i ≤ n
Γ′ = Γ{K1 : σ1 , . . . , Kn : σn } . (DATA)
hDATA(T :: κ, K1 : t1 , . . . , Kn : tn ), Γ, ∆, M i → hΓ′ , ∆′ , M ′ i
The DATA rule is interpreted operationally as follows:
1. Create a fresh type constructor µ, and record in the kind environment that
type name T stands for type µ with kind κ.
8 2. For each value constructor Ki with given type ti , confirm that type ti has
kind ∗, and translate it into an internal type scheme σi . This operation, ex‐
Userdefined, pressed by judgment ∆′ ` ti ; σi :: ∗, extends Typed µScheme’s judgment
algebraic types (and ∆′ ` ti :: ∗, which merely checks that the programmer’s type syntax is well
pattern matching) kinded. It is described below.
488 3. Also for each value constructor, check that its type scheme is compatible with
the new type being defined (σi ≼ µ :: κ).
4. Finally, assuming that all the types are compatible, enter the types of the
value constructors into the type environment.
Since PISCES has an incompatible type, the definition is rejected. Eliminate PISCES
and the definition is good:4
488b. htranscript 459ai+≡ ◁ 488a 493c ▷
‑> (data * fish
[BLUEGILL : fish] ; OK, you're a fish
[BASS : (sym ‑> fish)] ; OK, you return a fish
)
‑> BLUEGILL
BLUEGILL : fish@{2}
‑> (BASS 'largemouth)
(BASS largemouth) : fish@{2}
‑> (BASS 'striped)
(BASS striped) : fish@{2}
MONOIſCOMPAT MONORETURNſCOMPAT
σ ≼ µ :: κ
µ ≼ µ :: ∗ τ1 × · · · × τn → µ ≼ µ :: ∗
POLYIſCOMPAT
α1′ , . . . , αk′ all distinct
∀α1 , . . . , αk .(α1′ , . . . , αk′ ) µ ≼ µ :: ∗1 · · · ∗k ⇒ ∗
POLYRETURNſCOMPAT
α1′ , . . . , αk′ all distinct
∀α1 , . . . , αk .τ1 × · · · × τn → (α1′ , . . . , αk′ ) µ ≼ µ :: ∗1 · · · ∗k ⇒ ∗
4
Because every data generates a fresh type contructor, the new type prints as fish@{2}, which dis‐
tinguishes it from the original, bad fish.
KıNDINTROCON
T ∈ dom ∆ ∆(T ) = (τ, κ)
∆ ` t ; τ :: κ
∆ ` T ; τ :: κ
KıNDAPP
KıNDINTROVAR ∆ ` t ; τ :: κ1 × · · · × κn ⇒ κ §8.7
α ∈ dom ∆ ∆(α) = (τ, κ) ∆ ` ti ; τi :: κi , 1 ≤ i ≤ n Theory and
∆ ` α ; τ :: κ ∆ ` (t t1 · · · tn ) ; (τ1 , . . . , τn ) τ :: κ implementation of
userdefined types
KıNDFUNCTıON
∆ ` ti ; τi :: ∗, 1 ≤ i ≤ n ∆ ` t ; τ :: ∗ 489
∆ ` t ; σ :: ∗
∆ ` (t1 · · · tn ‑> t) ; τ1 × · · · × τn → τ :: ∗
SCHEMEKıNDALL
α1 , . . . , αn are all distinct SCHEMEKıNDMONOTYPE
∆{α1 7→ (α1 , ∗), . . . , αn 7→ (αn , ∗)} ` t ; τ :: ∗ ∆ ` t ; τ :: ∗
∆ ` (forall (α1 · · · αn ) t) ; ∀α1 , . . . , αn .τ :: ∗ ∆ ` t ; ∀.τ :: ∗
The type variables α1′ , . . . , αk′ , which are the parameters to µ, are actually a per‐
mutation of the quantified type variables α1 , . . . , αk . But in the compatibility judg‐
ment, it’s enough for the αi′ ’s to be distinct. If they are, the translation judgment
∆′ ` ti ; σi :: ∗ (below) ensures that they are a permutation of the αi ’s.
The compatibility rules are implemented by function validate in Appendix S.
Using that function, and using function txTyScheme to implement the translation
judgment described below, function typeDataDef types a data definition. It re‐
turns Γ′ , ∆′ , and a list of strings: the name T followed by names [K1 , . . . , Kn ].
489. htyping and evaluation of data definitions 489i≡ (S437a) 490 ▷ bind 305d
typeDataDef : data_def * type_env * (ty * kind) env type env 304
‑> type_env * (ty * kind) env * string list extendTypeEnv
S453c
fun typeDataDef ((T, kind, vcons), Gamma, Delta) =
freshTycon S449b
let hdefinition of validate, for the types of the value constructors of T S450ai type kind 355a
val mu = freshTycon T kindString S462d
val Delta' = bind (T, (TYCON mu, kind), Delta) snd S249b
fun translateVcon (K, tx) = (K, txTyScheme (tx, Delta')) txTyScheme S456c
val Ksigmas = map translateVcon vcons type ty 408
TYCON 408
val () = app (fn (K, sigma) => validate (K, sigma, mu, kind))
type type_env
Ksigmas 435a
val Gamma' = extendTypeEnv (Gamma, Ksigmas) typeSchemeString
val strings = kindString kind :: map (typeSchemeString o snd) Ksigmas S431d
in (Gamma', Delta', strings) validate S450a
end
Just as in Typed µScheme, type syntax written by a programmer isn’t trusted; non‐
sense like (int int) and (list ‑> list) is rejected by the type system. In Typed
µScheme, it suffices to check the kind of each type, then pass the syntax on to the
type checker. But in µML, the syntax of types is different from the internal repre‐
sentation used for type inference, so the syntax can’t be passed on. Instead, it is
translated.
The translation is expressed using two judgment forms, ∆ ` t ; τ :: κ and
∆ ` t ; σ :: ∗ , which translate µML type syntax into either a Hindley‐Milner
type or a type scheme, respectively. The rules are shown in Figure 8.6. The imple‐
mentation is so similar to Typed µScheme’s kind checking that I doubt you need to
8 see it. But if you do, you will find functions txType and txTyScheme in Appendix S.
The operational semantics and type theory of pattern matching apply to any lan‐
guage with pattern‐matching case expressions.
As illustrated in Section 8.2.1, a case expression is evaluated by trying one choice af‐
ter another, selecting the first choice whose pattern matches the scrutinee. If an at‐
tempt at pattern matching succeeds, it produces an environment ρ′ , which binds the
variables that appear in the pattern. If the attempt at pattern matching fails, I say it
produces † (pronounced “failure,” but think of a dagger in the heart). The † is not
an environment or a value or an expression or anything we have encountered be‐
fore; it is a new symbol that means pattern‐match failure. To stand for the result of
a pattern match, which is either an environment ρ′ or †, I use the metavariable r .
The matching judgment therefore takes the form hp, vi ↣ r :
Since patterns are matched only in the context of a case expression, understanding
of pattern matching begins with case expressions.
A case expression is evaluated by first evaluating the scrutinee e, which involves
no pattern matching. Once e is evaluated to produce a value v , the operational
semantics puts v back into the scrutinee position as a literal expression. This trick
Table 8.7: Correspondence between µML’s type system and code
(See also Table 7.3, page 432)
8 e
x
Expression
Variable
exp (page 486)
name (page 303)
K Value constructor vcon (page 486)
Userdefined, p Pattern pat (page 486)
algebraic types (and v Value value (page 486)
pattern matching)
ρ + ρ′ Extension <+> (page 305)
492 ρ1 ] ρ2 Disjoint union disjointUnion (page 497)
′
hp, vi ↣ ρ Pattern matches match(p, v) = ρ′ (page 494)
hp, vi ↣ † Pattern match fails match(p, v) raises Doesn'tMatch
ρ{x1 7→ v1 , . . . , xn 7→ vn } = ρ + {x1 7→ v1 , . . . , xn 7→ vn }.
Back to the case expression: what if the first pattern doesn’t match? Evaluation
continues with the next pattern. The operational semantics drops choice [p1 e1 ]
from the case expression, and it evaluates a new case expression whose first choice
is [p2 e2 ]:
hp1 , vi ↣ † hCAſE(LıTERAL(v), [p2 e2 ], . . . , [pn en ]), ρ, σi ⇓ v ′ .
hCAſE(LıTERAL(v), [p1 e1 ], . . . , [pn en ]), ρ, σi ⇓ v ′
(CAſEFAıL)
What if there are no more choices—that is, what if n = 1? Then no rule applies.
The operational semantics gets stuck, and the interpreter raises the RuntimeError
exception.
Each rule is implemented as a clause for function ev. To avoid an infinite loop,
the clauses for CAſE(LıTERAL(v), cs) must precede the clause for CAſE(e, cs).
The matching judgment is implemented by function match: if matching succeeds,
it returns ρ′ , and if not, it raises the exception Doesn'tMatch. In that case it’s time
to try the remaining choices.
492. hmore alternatives for ev for nanoML and µML 492i≡ (S429a) 493a ▷
| ev (CASE (LITERAL v, match : pat * value ‑> value env
(p, e) :: choices)) = <+> : 'a env * 'a env ‑> 'a env
(let val rho' = match (p, v)
in eval (e, rho <+> rho')
end
handle Doesn'tMatch => ev (CASE (LITERAL v, choices)))
If no choices match a LITERAL form, then the case expression does not match.
493a. hmore alternatives for ev for nanoML and µML 492i+≡ (S429a) ◁ 492 493b ▷
| ev (CASE (LITERAL v, [])) =
raise RuntimeError ("'case' does not match " ^ valueString v)
If the scrutinee e hasn’t yet been evaluated, ev calls itself recursively to evaluate e,
places the resulting value into a LITERAL expression, then tail‐calls itself to select a §8.8
choice. Theory and
493b. hmore alternatives for ev for nanoML and µML 492i+≡ (S429a) ◁ 493a implementation of
| ev (CASE (e, choices)) = case expressions
ev (CASE (LITERAL (ev e), choices))
493
Now that we know how matching is used, we can examine in detail, formally,
how it works. We start with a simple but common special case: a value constructor
applied to a list of variables. A pattern of the form (K x1 · · · xm ) matches values
of the form VCON(K, [v1 , . . . , vm ]). This case can be described by a specialized
rule:
x1 , . . . , xm all distinct
ρ = {x1 7→ v1 , . . . , xm 7→ vm } . (SPECıALıZED MATCH RULE)
h(K x1 · · · xm ), VCON(K, [v1 , . . . , vm ])i ↣ ρ
The simple case is easy: bind each variable to the corresponding value. But it’s
too simple; in real code, patterns can be nested to arbitrary depth. Nesting com‐
plicates the theory, but, as you can see in some of the traffic‐light examples in Sec‐
tion 8.1 and in Standard ML code throughout this book, nesting simplifies code.
If you want to understand why programmers like algebraic data types, experiment
with nested patterns. Once you’re convinced they are worth having, you’ll have an <+> 305f
CASE 486b
easier time with the theory.
cons B
In general, when a value constructor K appears in a pattern p, K is applied not Doesn'tMatch
to a list of variables but to a list of sub‐patterns: p = (K p1 · · · pm ). Each sub‐ 494b
pattern pi can introduce new variables, and bindings for all those variables have type env 304
ev S429a
to be combined. When combining bindings, the operational semantics avoids the
eval S429a
ambiguity that would arise if the same variable appeared in more than one binding: LITERAL S438a
it combines bindings using a new operation, disjoint union. match 494b
The disjoint union of environments ρ1 and ρ2 is written ρ1 ]ρ2 , and it is defined type pat 486c
reverse B
if and only if dom ρ1 ∩ dom ρ2 = ∅: rho S429a
RuntimeError
dom(ρ1 ] ρ2 ) = dom ρ1 ∪ dom ρ2 , S213b
type value 486d
ρ1 (x), if x ∈ dom ρ1
(ρ1 ] ρ2 )(x) = valueString S461b
ρ2 (x), if x ∈ dom ρ2 .
†]ρ=† ρ]†=† † ] † = †.
Like any other type system, µML’s type system guarantees that run‐time compu‐
tations do not “go wrong.” µML’s type system extends nano‐ML’s type system to
provide these guarantees:
• When a value is constructed using CONVAL, the value constructor in question §8.8
is applied to an appropriate number of values of appropriate types. Theory and
implementation of
• In every pattern, every value constructor is applied to an appropriate number case expressions
of sub‐patterns of appropriate types.
495
• In every case expression, every pattern in every choice has a type consistent
with the type of the scrutinee. And every variable in every pattern is bound
to a value that is consistent with the type and value of the scrutinee.
• In every case expression, the right‐hand sides all have the same type, which
is the type of the case expression.
• The introduction form for constructed data is the named value constructor.
Its typing rule is just like the typing rule for a named variable: look up the
type in Γ.
• The elimination form is the case expression, which includes patterns. The
typing rules for case expressions and patterns are the subject of this section.
Like nano‐ML, µML has two sets of typing rules: nondeterministic rules and
constraint‐based rules.
µML inherits all the judgment forms and rules from nano‐ML. Its basic nonde‐ bind 305d
terministic judgment form is still Γ ` e : τ . But case expressions and pattern CONPAT 486c
matching call for new judgment forms. The easiest form to explain is the one cons B
CONVAL 486d
that deals with a choice within a case expression; the form of the judgment is
disjointUnion
Γ ` [p e] : τ → τ ′ . In this form, type τ is the type of pattern p and type τ ′ is S453b
emptyEnv 305a
the type of expression e. Informally, the judgment says that if a case expression is
PVAR 486c
scrutinizing an expression of type τ , and if pattern p matches the value of that ex‐ WILDCARD 486c
pression, then in the context of that match, expression e has type τ ′ . The judgment
is used in the nondeterministic typing rule for a case expression:
Γ`e:τ
Γ ` [pi ei ] : τ → τ ′ , 1 ≤ i ≤ n .
(CAſE)
Γ ` CAſE(e, [p1 e1 ], . . . , [pn en ]) : τ ′
Every pattern pi has the same type as the scrutinee e, and every right‐hand side ei
has type τ ′ , which is the type of the whole case expression.
As in the dynamic semantics, the key judgment is a pattern‐matching judg‐
ment. And also as in the dynamic semantics, pattern matching produces an
environment—a type environment, not a value environment. But compared with
the dynamic semantics, the type system is more complicated:
• Type inference produces not only an output environment Γ′ , which gives the
8 types of the variables that appear in the pattern, but also a type τ , which is
the type of the whole pattern.
Userdefined, • As inputs, the dynamic semantics requires only the pattern and the value to
algebraic types (and be matched. In particular, the dynamic semantics requires no environment.
pattern matching) But type inference requires an input environment, which tells the system the
type of every value constructor that appears in the pattern.
496
The typing judgment for pattern matching therefore requires inputs p and Γ and
produces outputs τ and Γ′ . This two‐input, two‐output judgment form is written
Γ, Γ′ ` p : τ . The notation is inspired by the notation for typechecking an ex‐
pression (Exercise 33).
When p is a bare value constructor, it has the type it is given in the input envi‐
ronment, and it produces an empty output environment.
Γ`K:τ
(PATBAREVCON)
Γ, {} ` K : τ
The premise is a typing judgment for the expression K , which is a value construc‐
tor. Just like a value variable, a value constructor is looked up and instantiated:
Γ(K) = σ τ′ ⩽ σ .
(VCON)
Γ ` K : τ′
A wildcard pattern has any type and produces the empty output environment.
(PATWıLDCARD)
Γ, {} ` WıLDCARD : τ
A variable pattern also has any type, and it produces an output environment that
binds itself to its type.
(PATVAR)
Γ, {x 7→ τ } ` x : τ
The most important pattern is one that applies a value constructor K to a list of
sub‐patterns: p = (K p1 · · · pm ). The types of the sub‐patterns must be the
argument types of the value constructor, and the type of the whole pattern is the
result type of the value constructor. Each sub‐pattern pi can introduce new vari‐
ables; the environment produced by the whole pattern is the disjoint union of the
environments produced by the sub‐patterns (page 493). If the disjoint union isn’t
defined, the pattern doesn’t typecheck.
Γ ` K : τ1 × · · · × τm → τ
Γ, Γ′i ` pi : τi , 1 ≤ i ≤ m
Γ′ = Γ′1 ] · · · ] Γ′m
(PATVCON)
Γ, Γ′ ` (K p1 · · · pm ) : τ
The judgment for patterns is used in the rule for a choice [p e]. Typing pat‐
tern p produces a set of variable bindings Γ′ , and the right‐hand side e is checked
in a context formed by extending Γ with Γ′ , which holds p’s bindings:
Γ, Γ′ ` p : τ Γ + Γ′ ` e : τ ′ .
(CHOıCE)
Γ ` [p e] : τ → τ ′
The CHOıCE rule concludes the nondeterministic type theory of case expressions.
Constraintbased type inference for case expressions, choices, and patterns
C, Γ ` [p e] : τ → τ ′ CHOıCE
C, Γ, Γ′ ` p : τ C ′ , Γ + Γ′ ` e : τ ′
C ∧ C ′ , Γ ` [p e] : τ → τ ′
C, Γ, Γ′ ` p : τ PATVCON
T, Γ ` K : τK
Ci , Γ, Γ′i
` pi : τi , 1 ≤ i ≤ m
C = τK ∼ τ1 × · · · × τm → α, where α is fresh
C ′ = C1 ∧ · · · ∧ Cm Γ′ = Γ′1 ] · · · ] Γ′m
′ ′
C ∧ C , Γ, Γ ` (K p1 · · · pm ) : α
PATBAREVCON PATWıLDCARD PATVAR
T, Γ ` K : τ α is fresh α is fresh
T, Γ, {} ` K : τ T, Γ, {} ` WıLDCARD : α T, Γ, {x 7→ α} ` x : α
Figure 8.9: Constraint‐based rules for case expressions, choices, and patterns
The type of a value constructor vcon (K ) comes from pvconType (chunk S452d),
which instantiates vcon’s type scheme with fresh type variables.
The implementations of pattype and pattypes complete type inference for
case expressions. Inference code for the other expressions, except for value con‐
structors, is shared with nano‐ML. Type inference for a value constructor instanti‐
ates its type scheme with fresh variables; the code appears in the Supplement.
8.9.1 Syntax
Syntax varies. The implicit‑data form is used in Standard ML, OCaml, standard
Haskell, and Clean; the data form is used in Agda and Coq/Gallina. Both forms are
used in Idris and by the Glasgow Haskell Compiler. Spelling of value constructors
also varies; in Standard ML, value constructors are typically written in all capital
letters; elsewhere, they are typically written with a single, initial capital letter.
Examples of concrete syntax, including datatype definitions, case expressions,
and clausal definitions, can be found in any of the interpreters in this book from
Chapter 5 onward. As is typical, pattern matching and decision making are done
primarily by means of clausal definitions, not by case expressions. Because clausal
definitions look so much like algebraic laws, they are preferred.
Pattern matching is used in Erlang, even though Erlang does not have algebraic
data types (or even a type system). Erlang’s pattern matching is defined over terms;
the role of value constructors is played by atoms. Erlang includes both case expres‐ '() B
sions and clausal function definitions. bind 305d
conjoinCon‐
Pattern matching is so popular that it is sometimes used in other contexts. For straints 436d
example, in the multiparadigm language Scala, algebraic data types are encoded CONPAT 486c
using objects and classes, but the language also includes a case expression written cons B
disjointUnion
using the keyword match. As a more whimsical example, Nigel Horspool has used
S453b
Java exception handlers to implement pattern matching. DisjointUnion‐
Failed S453b
8.9.2 Additional checking: Exhaustiveness and redundancy emptyEnv 305a
FORALL 408
freshtyvar 434a
An algebraic data type is closed: once it is defined, new value constructors can’t be funtype S440e
added. Because each type is closed, a compiler can analyze each case expression patString S462a
and see if at run time, every possible value is guaranteed to be matched by some PVAR 486c
pvconType S452d
pattern. The analysis is called the exhaustiveness check. An exhaustiveness check
TRIVIAL 436a
is mandated by the Definition of Standard ML (Milner et al. 1997), but the Definition TypeError S213d
also requires that inexhaustive matches be let off with a mere warning. No such unzip3 S219a
latitude is extended to my students. WILDCARD 486c
When a case expression’s patterns are not exhaustive, evaluating the expression
can cause a run‐time error. Here’s an example:
499. htranscript 459ai+≡ ◁ 494a
‑> (define last (xs) last : (forall ['a] ((list 'a) ‑> 'a))
(case xs
[(cons y '()) y]
[(cons _ ys) (last ys)]))
‑> (last '(1 2 3))
3 : int
‑> (last '())
Run‑time error: 'case' does not match ()
When checking for exhaustiveness, a compiler may discover a pattern that can
never match. Such a pattern is often called redundant. (It’s redundant because it
can be removed without changing the behavior of the program.) Most compilers
warn of redundant patterns; confusingly, the Glasgow Haskell Compiler refers to
8 such patterns as “overlapped” or “overlapping.”
“Overlapping” is also used to describe a pair of patterns that match one or more
values in common. When patterns overlap, the first matching pattern is always
Userdefined,
chosen. Overlapping patterns are useful; for example, if just one case needs spe‐
algebraic types (and
cial handling, you can put a very specific pattern like (cons _ (cons _ zs)) before
pattern matching)
a general pattern like the wildcard, even though the specific pattern and the wild‐
500 card overlap. Non‐overlapping patterns are also useful; if no patterns overlap, then
patterns in a case expression or clauses in a definition can be written in any or‐
der, without changing the meaning of the program. But non‐overlapping patterns
are primarily a conceptual tool: they simplify equational reasoning. If you cared
whether patterns overlapped, your compiler could easily tell you, but no compiler
I know of actually does.
8.10 SUMMARY
Case expressions and pattern matching not only simplify conditional logic but §8.10
also reduce the amount of boilerplate needed to get at parts of product values Summary
(records). And clausal definitions, which use pattern matching, make it possible
for the definition of a function to look an awful lot like the function’s algebraic 501
laws. The succinctness and readability of pattern matching is highly valued by
many programmers—sometimes even more than type inference.
ALGEBRAıC DATA TYPE A data type whose values may be one of a set of enumerated
alternatives, in which each alternative is a CONſTRUCTED VALUE created by
a unique VALUE CONſTRUCTOR. Each of a type’s value constructors may be
applied to other values of given types, making the entire algebraic data type
a ſUM OF PRODUCTſ.
Algebraic data types were first explored in the experimental language HOPE, de‐
scribed by Burstall, MacQueen, and Sannella (1980). Among other concerns,
Burstall and his colleagues wanted to support user‐defined types, to help program‐
mers avoid forgetting cases, and to avoid cluttering the environment with names
for functions like null?, car, and cdr, which they replaced with pattern matching.
Algebraic data types don’t have to be closed: Millstein, Bleckner, and Cham‐
bers (2004) present a design that makes them extensible. Sadly, the design has not
caught on.
The type theory and operational semantics in this chapter draw heavily on the
Definition of Standard ML (Milner et al. 1997). The Definition is worth reading, but
it presents two challenges: First, it describes a whole language, the complexity of
which can’t be avoided. Second, its notation can be hard to follow: the rules rely not
only on subtle differences between forms of judgment but also on implicit premises
that are mentioned only briefly at the beginning of some sections. But the truth is
all there, and while I could not call the Definition elegant, I admire its parsimony.
Anyone reading the Definition is advised also to have the Commentary (Milner and
Tofte 1991), which contains not only discussion but also many worked examples of
problems similar to those in this book.
If you want to tackle the Definition of Standard ML, I owe you two additional cau‐
tions. First, I find the terminology challenging. For example, I believe that the Def
inition uses the words “type name” and “type constructor” the way I use the words
“type constructor” and “type name.” (I stand by my guns.) Second, the treatment
of generativity in the Definition is now widely believed to be inferior—it is too much
a description of an implementation, it has too much mechanism, and it may be too
difficult to reason about. A surprising alternative is to treat generative data defi‐
nitions as a special case of generative modules (Harper and Stone 2000). The idea
Table 8.10: Synopsis of all the exercises, with most relevant sections
is further developed in a very nice if very technical paper by Dreyer, Crary, and
Harper (2003).
Algebraic data types lend themselves to some nice extensions. In Appendix S,
you can read about existentially quantified value constructors and generalized al‐
gebraic data types (GADTs). Existential quantification is suggested by Mitchell and
Plotkin (1988) as a way of coding abstract types; its use with value constructors was
first suggested by Perry (1991). GADTs are beautifully introduced by Hinze (2003),
and some nice applications are presented by Pottier and Régis‐Gianas (2006) and
by Ramsey, Dias, and Peyton Jones (2010).
The compilation of ML pattern matching into decision trees was first described
by Baudinet and MacQueen (1985), who claim NP‐completeness and present a num‐
ber of heuristics. Scott and Ramsey (2000) present preliminary experiments sug‐
gesting that in practice, choice of heuristics may not matter; they also present
pseudocode for a match compiler. But the definitive work in this area is by
Maranget (2008), who carefully compares decision trees with backtracking au‐
tomata. Maranget also develops new heuristics and also a fine methodology for
experimental evaluation. In a separate paper, Maranget (2007) describes checks
for exhaustiveness and redundancy.
8.11 EXERCıſEſ
The exercises are summarized in Table 8.10. The highlights include some nice data
structures:
• The “zipper” (Exercise 16) is a purely functional data structure whose oper‐
ations have an imperative feel. It’s a classic. If you’re a beginner, the zipper
is a good challenge problem. If you have more experience and you have not
yet seen the zipper, you will find it very satisfying.
• Binary tries (Exercises 17 and 18) connect tries with binary representations
of integers in a way that has a nice theory and a nice implementation. And
ternary search trees (Exercise 19) show how case expressions and pattern
matching make it easy to code tree algorithms.
If you can, write the functions in this section using define*. You can implement it
yourself (Exercise 26), or if you are using this book for a class, your instructor may
have my implementation.
3. Create a list of pairs. Define a function zip that takes a pair of lists (of equal
length) and returns the list of pairs containing the same elements in the same
order. If the lengths don’t match, pass the symbol 'length‑mismatch to the
error primitive. Do not use if, null?, car, or cdr.
505c. hexercise transcripts 505bi+≡ ◁ 505b 506a ▷
‑> zip
<function> : (forall ['a 'b] ((list 'a) (list 'b) ‑> (list (pair 'a 'b))))
‑> (zip '(a b c) '(1 2 3))
((PAIR a 1) (PAIR b 2) (PAIR c 3)) : (list (pair sym int))
‑> (zip '(a b c) '())
Run‑time error: length‑mismatch
4. Nested patterns. If your zip from Exercise 3 includes more than one case ex‐
pression, reimplement it using a single case expression, while still adhering
to all the restrictions in Exercise 3.
8.11.4 Higherorder functions on lists and option
8 list xs, List.find returns (SOME v ) if xs contains a value v satisfying p?, and
NONE otherwise.
506a. hexercise transcripts 505bi+≡ ◁ 505c 506b ▷
Userdefined,
‑> List.find
algebraic types (and <function> : (forall ['a] (('a ‑> bool) (list 'a) ‑> (option 'a)))
pattern matching) ‑> (define positive? (n) (> n 0))
‑> (List.find positive? '(‑3 ‑2 ‑1 0 1 2 3))
506 (SOME 1) : (option int)
‑> (List.find (lambda (n) (> n 100)) '(‑3 ‑2 ‑1 0 1 2 3))
NONE : (option int)
8. Maps over option. Define function Option.map, which acts like map but works
on option values, not list values.
506d. hexercise transcripts 505bi+≡ ◁ 506c 507a ▷
‑> Option.map
<function> : (forall ['a 'b] (('a ‑> 'b) (option 'a) ‑> (option 'b)))
‑> (Option.map positive? NONE)
NONE : (option bool)
‑> (Option.map positive? (SOME 4))
(SOME #t) : (option bool)
‑> (Option.map reverse NONE)
NONE : (forall ['a] (option (list 'a)))
‑> (Option.map reverse (SOME '(1 2 3)))
(SOME (3 2 1)) : (option (list int))
9. Nested option. Define function Option.join, which takes an “option of
option” and returns a single option that is SOME whenever possible:
507a. hexercise transcripts 505bi+≡ ◁ 506d 507b ▷
‑> Option.join
<function> : (forall ['a] ((option (option 'a)) ‑> (option 'a)))
‑> (Option.join (SOME NONE))
NONE : (forall ['a] (option 'a))
‑> (Option.join (SOME (SOME 4)))
(SOME 4) : (option int) §8.11
Exercises
10. Comparison using order. Define a higher‐order sort function mk‑sort of type
(forall ['a] (('a 'a ‑> order) ‑> ((list 'a) ‑> (list 'a)))). Avoid
using if, null?, car, and cdr.
507b. hexercise transcripts 505bi+≡ ◁ 507a 507c ▷
‑> mk‑sort
<function> : (forall ['a] (('a 'a ‑> order) ‑> ((list 'a) ‑> (list 'a))))
‑> (mk‑sort Int.compare)
<function> : ((list int) ‑> (list int))
‑> (it '(0 2 1 5 5))
(0 1 2 5 5) : (list int)
‑> (val sort‑down (mk‑sort (lambda (n m) (Int.compare m n))))
‑> (sort‑down '(0 2 1 5 5))
(5 5 2 1 0) : (list int)
11. Merge sort via pattern matching. Without using if, null?, car, or cdr, define a
higher‐order mergesort function, which should have the same type as above:
(forall ['a] (('a 'a ‑> order) ‑> ((list 'a) ‑> (list 'a)))). I recom‐
mend defining a top‐level mergesort that takes compare as an argument, and
which contains a letrec that defines split, merge, and sort.
Unlike most sorting algorithms, mergesort requires two base cases: not
only is a list matching '() considered sorted, but so is a list matching
(cons x '()). Your sort function should therefore discriminate among
three cases—the two base cases and one inductive case. The inductive case
splits the list into two smaller lists, sorts each, and merges the results.
12. Higherorder functions with order. Comparison is useful not just on individual
values but on pairs, triples, lists, and so on. Such comparisons are typically
lexicographic: you compare the first elements, and if they are unequal, that’s
the result of the comparison. But if the first elements are equal, you compare Int.compare B
the remaining elements lexicographically. Try your hand at the higher‐order list6 B
functions below. Use case expressions, and look for opportunities to use the map B
wildcard pattern. reverse B
An algebraic data type defines a set of trees, and pattern matching is exceptionally
good for writing recursive functions on trees.
13. Binary trees. Implement a function bt‑depth that gives the depth of a binary
tree from Section 8.1 (page 463). An empty tree has depth zero; a nonempty
tree has depth 1 more than the maximum depth of its subtrees.
14. Binary search trees. A binary search tree can represent a finite map, which is to
say a set of key‐value pairs in which each key appears at most once. A binary
search tree is one of the following:
• The empty tree, which represents the empty set of key‐value pairs
• An internal node, which has a key, a value, a left subtree, and a right
subtree, and which represents the singleton set containing the given
key and value, unioned with the sets represented by the left and right
subtrees
Every binary search tree satisfies an order invariant. The empty tree satis‐
fies the order invariant by definition. An internal node satisfies the order
invariant if it has all of these properties:
(a) Define an algebraic data type that represents binary search trees. I rec‐
ommend that your algebraic data type include a value constructor that
takes no arguments and that represents an empty tree.
(b) Define an insert function that takes a key, a value, and a tree, and re‐
turns a new tree that is like the original tree, but binding the given key
to the given value. (In particular, if the given key is present in the old
tree, the new tree should associate that key with the new value.)
(c) Define a lookup function that takes a key and a tree. If the tree as‐
sociates the key with a value v , the function should return (SOME v ).
If not, the function should return NONE.
(d) Define a delete function that takes a tree and a key, and returns a tree
that is equivalent to the original, except that the key is not associated
with any value. If you are not familiar with deletion in binary search
trees, the usual heuristic is to reduce every case to the problem of delet‐
ing the key‐value pair from a given internal node. This problem divides
into three overlapping cases:
• If the left subtree is empty, the internal node can be replaced by
the right subtree.
• If the right subtree is empty, the internal node can be replaced by
the left subtree.
• If neither subtree is empty, delete the largest key‐value pair from
the left subtree. Form a new internal node using that key‐value
pair along with the modified left subtree and the original right sub‐
tree.
(e) Define a record type that holds insert, lookup, and delete functions.
Define a polymorphic function that takes a compare function and re‐ Int.compare B
turns such a record.
(f) Define a function treefoldr that does an inorder traversal of a binary
search tree. For example, given tree t,
(treefoldr (lambda (key value answer) (cons key answer)) '() t)
15. Sequences with fast append. List append takes time and space proportional
to the length of the left‐hand argument. In this problem, you define a new
representation of sequences that supports append in constant time.
Informally, a sequence of τ ’s is either empty, or it is a single value of type τ ,
or it is a sequence of τ ’s followed by another sequence of τ ’s.
(a) Use this informal definition to define an algebraic datatype seq of kind
8 (* => *). As with any abstraction that is defined by choices, your defi‐
nition should contain one value constructor for each choice.
(b) Define s‑cons, with type (forall ['a] ('a (seq 'a) ‑> (seq 'a))), to
Userdefined, add a single element to the front of a sequence, using constant time and
algebraic types (and space.
pattern matching)
(c) Define s‑snoc, with type (forall ['a] ('a (seq 'a) ‑> (seq 'a))), to
510 add a single element to the back of a sequence, using constant time and
space. (As always, snoc is cons spelled backward.)
Here the order of arguments is the opposite of the order in which the
results go in the data structure. That is, in (s‑snoc x xs), x follows xs.
The arguments are the way they are so that s‑snoc can be used with
foldl and foldr.
(d) Define s‑append, type (forall ['a] ((seq 'a) (seq 'a) ‑> (seq 'a))),
to append two sequences, using constant time and space.
(e) Define list‑of‑seq, with type (forall ['a] ((seq 'a) ‑> (list 'a))),
to convert a sequence into a list containing the same elements in the
same order. Function list‑of‑seq should allocate only as much space
as is needed to hold the result.
(f) Without using explicit recursion, define function seq‑of‑list, with
type (forall ['a] ((list 'a) ‑> (seq 'a))), which converts an ordi‐
nary list to a sequence containing the same elements in the same order.
(g) Ideally, function list‑of‑seq would take time proportional to the
number of elements in the sequence. But when a sequence contains
many empty‐sequence constructors, it can take longer. Prevent this
outcome by altering your solutions to maintain the invariant that the
empty‐sequence value constructor never follows or is followed by an‐
other sequence.
16. Emulating mutable lists. When lists are immutable, as they are in µScheme
and in the ML family, they appear not to support typical imperative opera‐
tions, like inserting or deleting a node at a point. But these operations can be
implemented on purely functional data structures, efficiently, in ways that
look imperative. The implementation uses a technique called the zipper.
You will learn the zipper by implementing a list with indicator. A list with
indicator is a nonempty sequence of values, together with an “indicator” that
points at one value in the sequence. Elements can be inserted or deleted at
the indicator, in constant time.
(a) Define a representation for type ilist of kind (* => *). Document your
representation by saying, in a short comment, what sequence is meant
by any value of type 'a ilist.
Given a good representation, the code is easy: almost every function
can be implemented as a case expression with one or two choices, each
of which has a simple right‐hand side. But a good representation might
be challenging to design.
(b) Support the documentation in part (a) by writing list‑of‑ilist:
510. hlistwithindicator functions 510i≡ 511a ▷
(check‑type list‑of‑ilist
(forall ['a] ((ilist 'a) ‑> (list 'a))))
(c) Define function singleton‑ilist, which takes a single value and re‐
turns a list whose indicator points at that value.
511a. hlistwithindicator functions 510i+≡ ◁ 510 511b ▷
(check‑type singleton‑ilist (forall ['a] ('a ‑> (ilist 'a))))
(d) Define function at‑indicator, which returns the value the indicator
points at.
511b. hlistwithindicator functions 510i+≡ ◁ 511a 511c ▷
(check‑type at‑indicator (forall ['a] ((ilist 'a) ‑> 'a)))
§8.11
Exercises
(e) To move the indicator, define indicator‑left and indicator‑right,
511
with these types:
511c. hlistwithindicator functions 510i+≡ ◁ 511b 511d ▷
(check‑type indicator‑left
(forall ['a] ((ilist 'a) ‑> (option (ilist 'a)))))
(check‑type indicator‑right
(forall ['a] ((ilist 'a) ‑> (option (ilist 'a)))))
Calling (indicator‑left xs) creates a new list ys that is like xs, ex‐
cept the indicator is moved one position to the left. And it returns
(SOME ys). But if the indicator belonging to xs already points to the
leftmost position, then (indicator‑left xs) returns NONE. Function
indicator‑right is similar. Both functions must run in constant time
and space.
These functions “move the indicator,” but no mutation is involved. In‐
stead of mutating an existing list, each function creates a new list.
(f) To remove an element, define delete‑left and delete‑right, with
these types:
511d. hlistwithindicator functions 510i+≡ ◁ 511c 511e ▷
(check‑type delete‑left
(forall ['a] ((ilist 'a) ‑> (option (ilist 'a)))))
(check‑type delete‑right
(forall ['a] ((ilist 'a) ‑> (option (ilist 'a)))))
Calling (delete‑left xs) creates a new list ys that is like xs, except
the element to the left of the indicator has been removed. And it re‐
turns (SOME ys). If the indicator points to the leftmost position, then
delete‑left returns NONE. Function delete‑right is similar. Both
functions must run in constant time and space, and as before, no mu‐
tation is involved.
(g) To insert an element, define insert‑left and insert‑right, with
these types:
511e. hlistwithindicator functions 510i+≡ ◁ 511d 512a ▷
(check‑type insert‑left
(forall ['a] ('a (ilist 'a) ‑> (ilist 'a))))
(check‑type insert‑right
(forall ['a] ('a (ilist 'a) ‑> (ilist 'a))))
Calling (insert‑left x xs) returns a new list that is like xs, except the
value x is inserted to the left of the indicator. Function insert‑right is
similar. Both functions must run in constant time and space. As before,
no mutation is involved.
(h) Define functions ffoldl and ffoldr, with these types:
512a. hlistwithindicator functions 510i+≡ ◁ 511e
(check‑type ffoldl
(forall ['a 'b] (('a 'b ‑> 'b) 'b (ilist 'a) ‑> 'b)))
8 (check‑type ffoldr
(forall ['a 'b] (('a 'b ‑> 'b) 'b (ilist 'a) ‑> 'b)))
Userdefined, These functions do the same thing as foldl and foldr, but on lists with
algebraic types (and indicators. They ignore the position of the indicator.
pattern matching)
To test these functions, start with the list test‑ilist defined below. The list
512 is created by a sequence of insertions, plus a movement. To emphasize the
imperative feel of the abstraction, test‑ilist is created by using let* to
rebind the name xs repeatedly. The code should remind you of a sequence
of assignment statements.
512b. hlistwithindicator test cases 512bi≡ 512c ▷
(val test‑ilist
(let* ((xs (singleton‑ilist 3))
(xs (insert‑left 1 xs))
(xs (insert‑left 2 xs))
(xs (insert‑right 4 xs))
(xs (case (indicator‑right xs) ((SOME ys) ys)))
(xs (insert‑right 5 xs)))
xs))
The resulting test‑ilist should be the list '(1 2 3 4 5), with the indicator
pointing at 4. It should pass these tests:
512c. hlistwithindicator test cases 512bi+≡ ◁ 512b
(check‑expect (list‑of‑ilist test‑ilist) '(1 2 3 4 5))
(check‑expect (at‑indicator test‑ilist) 4)
(check‑expect (Option.map list‑of‑ilist (delete‑left test‑ilist))
(SOME '(1 2 4 5)))
17. Tries as sets. A binary trie searches by looking at bits of an integer. A binary trie
can represent a set of integers, and set union, intersection, and difference
are easy to implement efficiently. You’ll implement a littleendian binary trie,
which looks at the least‐significant bits first.
A binary trie has three kinds of nodes: empty, singleton, and union.
{2 · k | k ∈ [[e]]} ∪ {2 · k + 1 | k ∈ [[o]]},
(a) Define an algebraic data type ints that represents a binary trie, which
in turn represents a set of integers.
(b) Define function ints‑member?, which tells whether an integer is in the
set.
(c) Define function ints‑insert, which inserts an integer into the set.
(d) Define function ints‑delete, which removes an integer from the set, §8.11
if present. Exercises
(e) Define function ints‑union, which takes the union of two sets. Find 513
an implementation that allocates fewer CONVALs than simply union by
repeated insertion.
(f) Define function ints‑inter, which takes the intersection of two sets.
(g) Define function ints‑diff, which takes the intersection of two sets.
(h) Define function ints‑fold, which folds over all the integers in a trie.
Give it type (forall ['a] ((int 'a ‑> 'a) 'a ints ‑> 'a)).
• The empty set can be represented not only as an empty trie, but also
as the union of two empty tries—or the union of any two tries that each
represent the empty set. The empty set is represented most efficiently
as the empty trie.
• A singleton set can be represented not only as a singleton trie, but also
as the union of a singleton trie and an empty trie—or the union of any
two tries that respectively represent the appropriate singleton set and
the empty set. A singleton set is represented most efficiently as a sin‐
gleton trie.
(i) Revisit the functions you have implemented above, and identify which
functions can create a union node of which one child is empty and the
other is either empty or singleton.
( j) Rewrite the functions you have implemented above so that no function
ever creates a union node of which one child is empty and the other
is either empty or singleton. The standard technique is not to use the
value constructor for union directly, but to define a smart constructor:
A smart constructor is a function that, at the abstract level, has the same
specification as a value constructor, but at the representation level, can
may be more efficient. For the binary trie, the smart constructor should
recognize two special cases: union of empty and empty should return
empty, and union of singleton n and empty should return either single‐
ton 2 · n or singleton 2 · n + 1. In other cases, the smart constructor
should behave like the value constructor for a union trie.
18. Tries as finite maps. Generalize your results from Exercise 17 to implement a
finite map with integer keys.
(a) To represent a finite map with integer keys, define an algebraic data
type intmap of kind (* => *).
(b) Define values and functions with these types:
514a. hexercise transcripts 505bi+≡ ◁ 508c 514b ▷
‑> (check‑type empty‑intmap
(forall ['a] (intmap 'a)))
19. Search trees with lists as keys. A ternary search tree combines aspects of a trie
and of a binary search tree. The original ternary search tree is specialized to
null‐terminated C strings (Bentley and Sedgewick 1997, 1998). The ternary
search tree that you will implement is polymorphic: it works with any type
of list.
A tree of type (ternary τa τb ) represents a finite map whose keys have type
(list τa ) and whose values have type τb . A ternary search tree has two
species of nodes: a decision node and a final node.
• A decision node contains a decision element d of type 'a, and it has three
children, each of which is also a ternary search tree:
– The left subtree stores all key‐value pairs whose keys are lists that
begin with an element that is smaller than d.
– The right subtree stores all key‐value pairs whose keys are lists that
begin with an element that is larger than d.
– The middle subtree stores all the key‐value pairs whose keys have
the form (cons d xs ). However, in the middle subtree, only the xs
part of the key is used—the d part is implicit in the position of the
middle subtree directly under a decision node.
In addition to its three subtrees, the decision node also stores the value
whose key is the empty list (if any).
• A final node stores only the value whose key is the empty list, if any.
It has no children.
20. Laws of tree traversal. Using equational reasoning, prove two laws about func‐
tion preorder‑elems, which is defined on page 463.
§8.11
(preorder‑elems BTEMPTY) = '() Exercises
(preorder‑elems (BTNODE x t1 t2)) = (cons x (append (preorder‑elems t1)
515
(preorder‑elems t2)))
21. Laws of case expressions. In all functional languages, one of the most impor‐
tant compiler optimizations is function inlining. Inlining sometimes results
in expressions that have a “case‐of‐case” structure:
(case
(case e [p1 e1 ] · · · [pk ek ])
[p′1 e′1 ]
.
.
.
[p′m e′m ]).
Write an algebraic law that will enable a compiler to rewrite such an expres‐
sion so that the scrutinee is not a case expression. Assume that all the ex‐
pressions are evaluated without side effects, so duplicating code is OK. (With
luck, any duplicate code will be eliminated by applications of laws described
in Section 8.3.)
In the exercises below, you implement the “patterns everywhere” syntactic sugar
described in Section 8.4. This sugar makes µML almost as expressive as core ML.
(“Almost” because µML does not support exceptions.) By and large, the exercises
don’t touch code related to evaluation or type checking; all they do is extend the
µML parser (Appendix S, page S462). The parsers in the appendix contain some
“hooks” and extra code that should make things easier—you can focus on the desug‐
aring functions.
23. Patterns for lambda parameters. Using syntactic sugar, extend µML’s lambda
expression so that each formal parameter is a pattern, not just a variable
(Section 8.4, page 480).
Even with this extension, many lambda expressions use only PVAR patterns.
Userdefined, These lambda expressions can be transformed using case, but if you instead
algebraic types (and convert them to ordinary lambda expressions without case, your code will
pattern matching) be easier to debug.
516 24. Patterns for define parameters. Using syntactic sugar, extend µML’s define
form so the formal‐parameter list is a list of patterns, not just a list of vari‐
ables (Section 8.4, page 480). Replace the formals parser with patFormals,
and rewrite the define function. As in Exercise 23, use freshVars, tupleexp,
and tuplepat—if you’ve done Exercise 23, you may be able to reuse code.
And as in Exercise 23, if all the patterns are PVAR patterns, it’s better not to
introduce a case expression.
25. Clausal lambda expressions. Using syntactic sugar, implement the lambda*
expression (Section 8.4, page 482). Don’t change any parsers; just update
the ML code in Appendix S to replace the lambdastar function with one
that returns OK e, where e is a lambda expression. As above, use freshVars,
tupleexp, and tuplepat to good advantage. And your lambdastar function
should check that each list of patterns has the same length (shown as n on
page 482). The check isn’t strictly necessary, but if it is omitted and the
lengths aren’t equal, the error messages are baffling.
27. Pattern binding in let expressions. Using syntactic sugar, implement the
transformation that enables let expressions to bind patterns, not just vari‐
ables (Section 8.4, page 482). Rewrite the letx expression‐builder func‐
tion in Appendix S, which should now take a (pat * exp) list instead of a
(name * exp) list. Your new letx function must handle three cases:
You will also need to replace these parsers in the exptable function:
does not include a struct definition, because no fields are given. But the
declaration
517b. hgenerativity.c 517ai+≡ ◁ 517a
double distance_from_origin(struct point {double x; double y; });
does include a struct definition, because fields are given—even though those
fields are in unusual places.
This exercise will help you discover how generativity works in C and how
your favorite C compiler deals with it.
(a) Write a C program that contains at least two distinct struct definitions,
both of struct point, and both containing two double fields x and y.
(b) Extend your C program so that you try to use one struct point where
the other one is expected, causing a compile‐time error.
(c) Find a C compiler that complains about your program of part (b), say‐
ing that it expected struct point but found struct point instead—or
something similar.
(a) Consult the language definition for Modula‐3—try not to distract your‐
self with subtyping—and design a similar branded mechanism for µML.
Update the formal system to account for brands, and give additional
rules for the translation of type syntax into types.
(b) Write rules for type equivalence that account for brands.
(c) Write introduction and elimination rules for expressions that have
branded types. This part of the exercise may suggest to you why the
designers of Modula‐3 limited branding to reference types.
Userdefined, 31. Kinds. Show that µML’s kind system is a conservative extension of Typed
algebraic types (and µScheme’s kind system. Start with a definition of erasure:
pattern matching)
• The erasure of the empty environment is the empty environment.
518 • If ∆ˆ is the erasure of environment ∆, then the erasure of environment
′
∆ = ∆{T 7→ (τ, κ)} is ∆{T ˆ 7→ κ}.
• The erasure of judgment ∆ ` t ; τ :: κ is judgment ∆ ` τ :: κ.
32. Type abbreviations. As your programs get more sophisticated and types get
bigger, you will wish for type abbreviations. In this exercise, you get them.
You will change the rules and the code for the type translation on page S455.
(a) Change environment ∆ so that a type name T may stand for one of two
things:
• A type
• A function from a list of types to a type
(b) To keep things simple, change the KıNDAPP rule so that only a type
name T can be applied. The rule will then work as long as T stands for
a type.
(c) When T stands for the function λα1 , . . . , αn .τ , use this new rule:
34. Patternmatch failures. Using the operational semantics, prove that if there
is a derivation of the judgment hp, vi ↣ †, then the derivation contains an
instance of either the FAıLVCON rule or the FAıLBAREVCON rule.
35. Type inference for patterns. The constraint‐based rules for typechecking pat‐
terns (Figure 8.9, page 498) do more bookkeeping than they have to. The con‐
straint can be eliminated: instead of judgment C, Γ, Γ′ ` p : τ , new rules
can use the simpler judgment Γ, Γ′ ` p : τ . Constraint C is eliminated by
finding a substitution θ that solves C and whose domain is the free variables
of C . The original derivation is transformed by applying θ to it. The resulting
judgment about p is θC, Γ, θΓ′ ` p : θτ , and since θC = T by definition,
this transformation leads to the simpler judgment form.
The hard part of the exercise is to show that applying θ doesn’t affect the type
that is inferred in the rest of the derivation.
(e) Write new rules for the judgment form Γ, Γ′ ` p : τ . Except for choice
of fresh type variables, your rules should be deterministic. Rules PAT‐
BAREVCON, PATWıLDCARD, and PATVAR can be rewritten simply by re‐
moving the trivial output constraint. The PATVCON rule needs to solve
constraint C and apply the resulting substitution to Γ′ and α. (Con‐
straints C1 , . . . , Cm are all trivial, and therefore so is constraint C ′ .)
(f) Simplify the CHOıCE rule.
(g) Rewrite the code to reflect your new, simpler rules.
Userdefined, (a) Extend the parser, type checker, and evaluator of µML to support
algebraic types (and integer‐literal patterns.
pattern matching)
(b) Using patterns, write a recursive Fibonacci function that does not
520 use if.
38. Exhaustive or redundant patterns. Section 8.9.2 on page 499 describes checks
for exhaustive and redundant patterns. Given a scrutinee of type τ , a set of
patterns is exhaustive if every value that inhabits type τ is matched by some
pattern. In this exercise you write an analysis that prints a warning message
when it finds a non‐exhaustive or redundant pattern match.
The idea is this: Suppose you have the set of all possible values of type τ .
And suppose that, for each member of this set, you can tell if it does or does
not match a particular pattern p. Then you can check for both exhaustiveness
and redundancy using an iterative algorithm that maintains a set of all values
not yet matched.
There’s only one snag: for interesting types, the sets are infinite. To represent
such sets, I define a “simple value set” to be one of two choices:
A full set of values is a collection of simple sets, using the collections defined
in Section H.2.4 (page S218).
520. hexhaustiveness analysis for µML 520i≡ (S420b) 521b ▷
datatype simple_vset = ALL of ty
| ONE of vcon * simple_vset list
type vset = simple_vset collection
In the analysis, each set of values is classified according to whether it does
or does not match a pattern. Full sets can be classified, simple sets can be
classified, and even lists of sets can be classified. Start with this code:
521a. hexhaustiveness analysis for µML [[prototype]] 521ai≡ 522a ▷
classifyVset : pat ‑> vset ‑> (bool * simple_vset) collection
classifySimple : pat ‑> simple_vset ‑> (bool * simple_vset) collection
fun classifyVset p vs = joinC (mapC (classifySimple p) vs)
and classifySimple p vs = raise LeftAsExercise "match classification"
§8.11
Exercises
Classification takes a single value set as input and produces a collection as
output. A simple value set vs is classified by a pattern p as follows: 521
Function call vconsOf mu returns the name and type scheme of each
value constructor associated with mu. Function vconsOf is omitted from
this book.
(a) Implement classifySimple, but leave out the case of matching value
constructors.
The replacement should compute successive value sets that are as yet
unmatched. Start with the value‐set collection singleC (ALL tau), and
classify value sets using each pattern from ps in turn. Issue a warning
message for each redundant pattern, and if the list of patterns is not
exhaustive, issue a warning for that too.
In the languages we’ve examined so far, when we have a high‐level problem like
“see if a list contains an interesting element,” we can define a high‐level, problem‐
specific function like exists?. But we can’t yet define problem‐specific data;
no matter what problem we’re working on, our code is written in terms of repre
sentations like numbers, symbols, Booleans, lists, S‐expressions, and constructed
data. We should hope for better; if we’re implementing high‐level actions like “find
the rule in the table,” “multiply two 50‐digit numbers,” or “stop recording when the
event is over,” then our code should be written in terms of abstractions like tables,
large numbers, and events. Such abstractions can be defined by the language fea‐
tures described in the last two chapters of this book.
The ability to define a new form of data, hiding its true representation from
most code, is called data abstraction. Data abstraction is typically supported by ab
stract data types, objects, or a combination thereof. Using abstract data types, code
is organized into modules, and only a function that is inside a module has access to
the representation of values of that module’s abstract types. Using objects, code is
organized into methods, and only a method that is defined on an object has access to
the representation of that object. Abstract data types and modules are described in
this chapter, which also describes some proven strategies for designing programs
that use data abstraction. Objects and methods are described in Chapter 10.
Why use data abstraction? It enables us to replace a representation with a new
one, without breaking code; it helps limit the effects of future changes; and it pro‐
tects vulnerable representations from most careless or faulty code. For example,
• The Linux kernel (and much else besides) is built by a program called Make.
Make works by composing rules, which say how to build the components of a
system. In Make’s initial implementation, rules were kept on an association
list, which was fine, because Makefiles started out small: a Makefile con‐
tained at most one or two dozen rules. But Make’s author, Stuart Feldman,
decided that for the first public version, craftsmanship demanded a hash ta‐
ble. Because the representation was hidden, it was easy to replace.
Did it matter? When Feldman was asked to help debug a Makefile, he found
thousands of rules. Luckily, the hash table performed well—far better than
525
the association list would have. Data abstraction enabled Feldman to replace
one representation with another, without breaking code.
9 real‐time game information, it records until the game is actually over, not
just until the end of the game’s scheduled time slot. But real‐time game in‐
formation often changes format; first it’s HTML, next it’s XML, now it’s JSON.
Molecule, abstract Its content changes, too. These changes break my code, but I limit their ef
data types, and fects by defining an abstraction that tells me only who’s playing, whether the
modules game has started, and whether it’s over. Only the abstraction knows the ever‐
changing representation of game information, and when the representation
526
changes again, I can update my code quickly—without missing any games.
Abstractions and invariants are the key elements in a proven strategy for designing
with data abstraction, which is sketched in Section 9.6.
Data abstraction can be accomplished using abstract types, objects, or both, but
because abstract types are easier to learn, they are where we begin. Every abstract
type is defined in some module, and its representation is exposed only to functions
that are defined in that same module. Functions defined outside the module, which
are called client functions, know only the name of the abstract type, not its repre‐
sentation. When a client function wants to create, observe, or mutate a value of
abstract type, it must call a function defined inside the module. Types and func‐
tions defined in a module are visible to client code only if they are named in the
module’s interface; such types and functions are said to be exported.
Abstract data types militate toward a particular programming style, which you
already have some experience with: in the C interpreters in Chapters 1 to 4, you
have seen abstract types Name and Env, as well as C’s built‐in FILE *. Primitive types,
such as Typed µScheme’s int or C’s float and double, also behave like abstract
types—that is, all we know about these primitive types are their names. To work
with primitive types, we must use primitive functions, which have privileged access
to the representations.
In this book, abstract data types are illustrated by the programming language
Molecule, which borrows from the Modula, ML, and CLU families of languages.
Molecule’s key feature is abstract data types, but to enable you to program inter‐
esting examples, Molecule provides two other notable features. First, it supports
polymorphism through generic modules. Second, to enable check‑expect testing
and a read‐eval‐print loop without violating abstraction, Molecule overloads names
like = and print, among others.
A caution: When programs are built from modules and are full of abstractions,
programming feels different. Molecule emulates languages like Ada, CLU, and
Modula‐3, and unlike Scheme, ML, and Smalltalk, these languages don’t help you
knock out small, interesting programs quickly—they help you build large systems
that can evolve. But in one chapter of a survey book, we can’t build a large system,
and we can’t realistically show how software evolves. To appreciate the software‐
engineering benefits that modules provide, you must use your imagination.
Language design for modules
A language for learning about modules should include distinct syntax for inter‐
faces and implementations, a clean type theory, a relatively simple core layer,
and support for generic (parameterized) modules. Unfortunately, no single lan‐
guage found in the wild meets all three criteria. To create a language for learn‐
ing about modules, I borrowed from the Modula family, which pioneered sep‐ §9.1
arately compiled interfaces and implementations, from Xavier Leroy’s formu‐ The vocabulary of
lation of ML modules, which exemplifies simple, clean generic modules with data abstraction
modular type checking, and from CLU, which demonstrated the benefits of ab‐
527
stract types. Because the name “Moleclu” would be hard to pronounce, the re‐
sulting language is called Molecule.
To program with data abstraction, we must understand our programs at two levels:
the concrete level, which explains details that appear in the code, and the abstract
level, which explains the ideas that drive the code’s design. Relating these levels
requires some vocabulary.
The key word is abstraction. An abstraction is the thing that we want to write
most of our code in terms of: a ball game, a large number, a table, or what have you.
Narrowly, “an abstraction” is the thing that a value of abstract type stands for.
An abstraction isn’t code; it’s an idea that tells us what code is doing. An abstraction
might be described using mathematics or metaphor.
Representation describes the data in the code. For example, a ball game in my
video recorder is represented by a JSON object of unholy complexity.
Access to representation determines whether code operates at an abstract level
or a concrete level. Code that can see only an abstraction is abstract; it is called
client code. Code that can see the representation of an abstraction is concrete; it is
part of the abstraction’s implementation.
When using abstract data types, an implementation is called a module. Or it
should be—in practice, modules are called different things in different languages.
For example, in the Modula family a module is an “implementation module,” in
Ada it’s a “package body,” and in Standard ML it’s a “structure.” In Molecule, it’s a
module.
A module grants access to its clients through an interface. An interface describes
the abstractions implemented by a module and the operations on the abstractions.
In the world of abstract data types, operations are functions; in the world of objects,
operations are methods. Either way, operations described in an interface are said
to be exported or public. For example, the operations exported by the ball‐game
module are to get a list of games from the Internet and to ask a single game who is
the home team, who is the away team, if the game has started, or if it is over.
The word “interface” always means “what you need to know to use a module.”
But what you truly need to know depends on what you want to do. If you want to
write client code, you need to understand the abstraction, and you need to know the
complete specification of every operation: its name, its type, and how it is supposed
to behave. This information is an “application program interface” (API). Behavioral
specifications are usually written informally, in natural language; they might be
found on a web page of developer documentation, or for an older interface, on a
Unix man page. When I want to emphasize this meaning of “interface,” I use “API”
or “complete API.”
Table 9.1: Examples of qualified names
Name Component
9 IntArray.t
IntArray.size
The type of an array of integers
Function that gives the size of an array of integers
IntArray.new Function that creates a new array of integers
Molecule, abstract Int.t The type of an integer
data types, and Int.< Function that compares two integers
modules Bool.t The type of a Boolean
528
If you just want code to compile, then all you need to know are the names and
types of the operations. This information is usually written formally, in a program‐
ming language, where it can be checked by an interpreter or compiler; the language
construct used to express it is also called an “interface.” Or it should be—in prac‐
tice, interfaces, which are bit like C’s .h files, are called different things in different
languages. For example, in the Modula family an interface is a “definition module,”
in Ada it’s a “package specification,” in Standard ML it’s a “signature,” and in Java
it’s an “interface.” In Molecule, it’s a module type. When I want to emphasize this
meaning of “interface,” I use “module type.”
Finally, “abstraction” often refers not just to what a value of abstract type stands
for, but also to the things around it. When a programmer says, “I’m going to design
an abstraction,” they mean at least the abstraction and its complete API, and maybe
also an implementation.
Client code refers to types and operations that are defined in a module. Each such
defined thing is called a component. Client code may refer to any component that
is exported by a module’s module type. Every component has a name, and every
module also has a name, and the two together are used to form the component’s
qualified name. The qualified name is the name of the module, followed by a dot,
followed by the name of the component, as in IntArray.new, which is an operation
that creates a new array. If you are used to dot notation for the members of a struct
or record, as in C, you may think of a module as a sort of glorified record, which
includes not only value components but also type and module components.
Some examples of qualified names appear in Table 9.1. The t in Int.t is
conventional: when a module exports one, primary abstract type—like an array,
a list, or a hash table, for example—the abstract‐type component is conventionally
called t. For example, Int.t is the type of integers, IntArray.t is the type of arrays
of integers, IntList.t is the type of lists of integers, and so on. This convention is
borrowed from Modula‐3 (Nelson 1991; Horning et al. 1993; Hanson 1996).
The .t convention is useful, but for continuity, you can continue to refer to
primitive types using the unqualified names that are used in other chapters: int,
bool, sym, and unit. In Molecule, these names are defined as type abbreviations:
529a. hpredefined Molecule types, functions, and modules 529ai≡ 529b ▷
(type int Int.t)
(type bool Bool.t)
(type unit Unit.t)
(type sym Sym.t)
§9.2
Introduction to
What any client must know: An example interface
Molecule, part I:
Client code depends on a complete API, including a description of the abstraction, Writing client code
a module type, and a behavioral specification of every exported operation. The ex‐ 529
ample client below depends on the IntArray module, whose abstraction is an array
of integers. The abstraction, which I hope is familiar, can be described using this
metaphor: A value of type IntArray.t (or just “an array”) is a sequence of boxes,
each of which holds a value of type int.
The IntArray module implements the ARRAY module type, which is part of the
initial basis of Molecule and is defined here:
529b. hpredefined Molecule types, functions, and modules 529ai+≡ ◁ 529a 540a ▷
(module‑type ARRAY
(exports [abstype t] ;; an array
[abstype elem] ;; one element of the array
[new : (int elem ‑> t)]
[empty : ( ‑> t)]
[size : (t ‑> int)]
[at : (t int ‑> elem)]
[at‑put : (t int elem ‑> unit)]))
The complete API for arrays specifies not just the type but also the behavior of each
exported operation. Crucially, the specifications are written in terms of the ab‐
straction (the box metaphor).
• Calling (new n v ) creates an array of n boxes, numbered from 0 to n − 1.
Each box holds the value v .
• Calling (empty) creates a new, empty array with no boxes in it.
• Calling (size A) tells us how many boxes are in array A.
• Calling (at A i) tells us what value is in box i of array A.
• Calling (at‑put A i v ) puts value v into box i of array A.
In Molecule, as in OCaml and Standard ML, a module type has its own identity,
independent of any module that might implement it. And the ARRAY module type
may be implemented by many modules: array of integers, array of Booleans, array
of symbols, and so on. A module type is a species of promise, which any implemen‐
tation must redeem. The promise is to provide each of the exported components
listed in the module type. Therefore, a module can implement the ARRAY module
type if it provides two types t and elem, plus the five functions new, empty, size,
at, and at‑put—all with the correct types.
In Molecule, a module can implement more than one module type. The most
specific module type that a module can implement is specified when the module
is defined. For IntArray, the module type ARRAY is not quite specific enough:
it doesn’t say what elem is. To write a client that finds the smallest integer in
an array, we need to know not only that IntArray.at returns a value of type
IntArray.elem; we also need to know that type IntArray.elem is the same as
type int. In Molecule, elem’s identity is revealed by a manifesttype declaration:
9 The IntArray module implements both the ARRAY module type and a module
type that reveals elem, as confirmed by these unit tests:
530a. hinterface checks of predefined modules 530ai≡ 540b ▷
Molecule, abstract (check‑module‑type IntArray ARRAY)
data types, and (check‑module‑type IntArray (exports [type elem Int.t]))
modules
Example client code
530
Knowing that IntArray implements module type ARRAY with type elem equal to int,
client code can use the exported operations, referring to them by their qualified
names. As an example, I use IntArray.new to make an array:
530b. htranscript 530bi≡ 530c ▷
‑> (val a (IntArray.new 4 99))
[99 99 99 99] : IntArray.t
Molecule code is quite similar to Typed µScheme code, but as illustrated here, Mol‐
ecule has some additional features that simplify procedural programming. One is
the when form, which is “an if with no else”; its body is executed only when the
condition holds. Another is that the body of a let expression, a while loop, or
a function definition may be a sequence of expressions. At parse time, each such
sequence is wrapped in an implicit (begin · · · ).
The smallest‑int function can be used with our example array a. And mutat‐
ing a can change the outcome:
530d. htranscript 530bi+≡ ◁ 530c 542 ▷
‑> a
[99 99 99 99] : IntArray.t
‑> (smallest‑int a)
99 : Int.t
‑> (IntArray.at‑put a 1 55)
‑> (IntArray.at‑put a 2 33)
‑> (smallest‑int a)
33 : Int.t
The hard part of programming with data abstraction is thinking up good abstrac‐
tions and their specifications—once an abstraction is specified, implementing it is
comparatively easy. I would love to demonstrate using IntArray, but IntArray is
not actually implemented in Molecule; the array operations are implemented by
primitive functions, which are implemented using ML code in the interpreter. In‐
stead, I demonstrate with a module that implements a new, less useful abstraction:
a point in the two‐dimensional plane.
§9.3
Introduction,
Interface and client code for a point abstraction part II:
Implementing an
A complete API for two‐dimensional points includes a module type that lists the
abstraction
exported operations. Each operation’s behavior is specified in a comment.
531a. h2dpoint.mcl 531ai≡ 532b ▷ 531
(module‑type 2DPOINT
[exports
[abstype t] ;; a point on the plane
[new : (int int ‑> t)] ; takes (X, Y) coordinates and
; returns a new point
As seen by the types and the comments, the abstraction is mutable: reflection and
rotation change the state of an existing point.
The API can be illustrated by some simple examples. First, some bureaucracy:
load the code from file 2dpoint.mcl, then use overload to tell the interpreter to
print points using function 2Dpoint.print:
531b. h2Dpoint transcript 531bi≡ 531c ▷
‑> (use 2dpoint.mcl)
module type 2DPOINT = ...
...
‑> (overload 2Dpoint.print) ;; tell the interpreter how to print a 2Dpoint.t
overloaded print : (2Dpoint.t ‑> Unit.t)
Z -
Molecule, abstract Z x
data types, and Z
Z
modules Zr
after rotate+ and reflect,
532 p = (4, −3)
Reflecting p through the origin negates both coordinates, leaving p in the lower‐
right quadrant.
532a. h2Dpoint transcript 531bi+≡ ◁ 531d
‑> (2Dpoint.reflect p)
‑> p
(4, ‑3) : 2Dpoint.t
‑> (2Dpoint.quadrant p)
lower‑right : Sym.t
The use of 2DPOINT here does more than just say, “module 2Dpoint implements
the 2DPOINT interface.” It also seals the module. Sealing prevents information from
escaping; only what’s exposed in the module type is visible.
The sealing is checked by the Molecule interpreter, which confirms that the
2Dpoint module redeems the promises made by the 2DPOINT module type. Each
promise made by a module type is redeemed as follows:
This definition says that a value of type 2Dpoint.t is constructed by applying value §9.3
constructor XY to two values of type int. In Molecule, by contrast with µML, con‐ Introduction,
structed values are mutable; the value constructor allocates two fresh, mutable lo‐ part II:
cations and stores an argument in each one. These locations become accessible Implementing an
through pattern matching in a case expression. abstraction
Value constructor XY is used in function 2Dpoint.new to create a new point from
533
the given coordinates x and y.
533b. hdefinitions of functions inside module 2Dpoint 533bi≡ (532b) 533c ▷
(define t new ([x : int] [y : int])
(XY x y))
Because this code is inside the 2Dpoint module, it can use constructor XY. Because
the constructor is not exported by the interface, client code can’t use it.
Inside the module, value constructor XY can also be used for pattern matching,
as it is in functions get‑x and get‑y.
533c. hdefinitions of functions inside module 2Dpoint 533bi+≡ (532b) ◁ 533b 533d ▷
(define int get‑x ([p : t])
(case p [(XY x y) x]))
Now let’s see how a point is mutated. To reflect a point through the origin,
I negate both x and y coordinates, using function Int.negated from Molecule’s
predefined Int module. The pattern matching on point p makes names x and y
refer to the mutable locations holding p’s coordinates. Like the locations named by
other variables, these locations can be set.
533d. hdefinitions of functions inside module 2Dpoint 533bi+≡ (532b) ◁ 533c 533e ▷
(define unit reflect ([p : t])
(case p [(XY x y) (begin
(set x (Int.negated x))
(set y (Int.negated y)))]))
The rotate+ operation also mutates a point, replacing x with −y and y with x.
To calculate new values of x and y before any location is mutated, I use a let form:
533e. hdefinitions of functions inside module 2Dpoint 533bi+≡ (532b) ◁ 533d 533f ▷
(define unit rotate+ ([p : t])
(case p [(XY x y) (let ([new‑x (Int.negated y)]
[new‑y x])
(set x new‑x)
(set y new‑y))]))
534
9.4 THE MOLECULE LANGUAGE
• The core layer is stuff you already know from Chapters 6 and 8: it’s Typed
µScheme plus algebraic data types from µML. And the core‐layer part of the
type system is a lot like Typed Impcore’s type system; there’s nothing fancy.
• Only the module layer has to worry about enforcing data abstraction. And
it’s reusable: the module layer, which is based on the work of Leroy (2000),
could be grafted onto a different core layer and it still do its job.
To keep the type system as simple as possible, both layers share a single name
space (environment), which is used for everything: values, types, and modules.
A name that originates in the core layer may stand for a mutable location holding
a value, as in Typed Impcore or Typed µScheme, or it may stand for a type; type
names are not relegated to a separate environment as they are in Typed µScheme.
A name that originates in the module layer may stand for a module or a module
type. Finally, a name may be overloaded (Section 9.4.3), standing for a set of values
of different types.
Molecule’s core layer is very much like Typed µScheme (Chapter 6). The core layer
combines familiar elements (Chapters 6 and 8) in new ways:
• Molecule’s core layer includes the familiar syntactic forms from Impcore
and µScheme: set, if, while, begin, function application, let forms, and
lambda. For deconstructing constructed data, it also includes the case form
from µML. And to better support procedural programming, it provides syn‐
tactic sugar for sequencing and conditionals.
Corelayer types
§9.4
Molecule’s support for polymorphism resides in the module layer; the core layer The Molecule
is monomorphic. A core‐layer type is either a type constructor or a function language
type—there are no type parameters, no type variables, and no polymorphic types.
535
The typing rules are exactly what you would expect from Typed µScheme, and they
are very close to Typed Impcore.
Molecule’s core layer includes algebraic data types like those in µML (Chap‐
ter 8), but as in the rest of the core layer, there are no type parameters. Every
algebraic data type has kind ∗.
Like µML but unlike Typed µScheme and Typed Impcore, Molecule enables
programmers to create new type constructors. A new type constructor can be cre‐
ated by a data form, as in µML. And, uniquely to Molecule, a new type constructor
can be created by sealing a module (typing rules in Section 9.8).
Corelayer values
Molecule has all the values found in µScheme, plus a form of constructed data.
Unlike in µML, where a value constructor carries other values, in Molecule, a value
constructor carries mutable locations. A constructed value is introduced either by
using a value constructor alone, like #f, or by applying a value constructor to ar‐
guments, as in (XY 3 4). When a value constructor is applied, a fresh, mutable
location is allocated for each argument.
A constructed value is eliminated via pattern matching in a case expression.
When a pattern names the argument of a value constructor, that name refers to the
mutable location that is carried with the value constructor. Such a location can be
mutated using set, as in the implementation of rotate+ in Section 9.3.
The core layer’s concrete syntax is shown in Figure 9.3 on the next page. It is
the same as the syntax for Typed µScheme (Figure 6.3, page 353), except for these
changes:
• The exp form that names a variable or function may use a qualified name,
not only a simple name. Similarly, the typeexp form that names a type con‐
structor may use a qualified name.
• To support algebraic data types, Molecule includes the data definition form,
the case expression form, and the pattern syntactic category from µML (Fig‐
ure 8.1, page 467).
• Because its core layer is monomorphic, Molecule does not have Typed
µScheme’s type‑lambda or @ expression forms. And Molecule does not have
Typed µScheme’s type variables or forall types.
• In addition to familiar syntactic sugar for && and ||, Molecule provides syn‐
tactic sugar that helps with procedural programming (Figure 9.5). Forms
when and unless implement one‐way conditionals, and each of these forms,
as well as the while, let, and define forms, supports a body that is a se‐
quence of expressions, not just a single expression.
The core‐layer definition forms shown in Figure 9.3 are supplemented by module‐
layer definition forms, which appear in Figure 9.4 and are described below.
Corelayer evaluation
The core‐layer forms are evaluated as in µScheme or µML, with extra support for
overloading (Section 9.4.3). The new forms, which are syntactic sugar, are evalu‐
ated as follows:
• The assert form evaluates its exp, and if the exp is not true, halts the program
with a checked run‐time error.
• The when form evaluates its condition, and if the condition is true, evaluates
every expression in the body, in sequence. The unless form is similar, except
the body is evaluated only if the condition is false.
A module type both controls access to a module and makes promises on the mod‐
ule’s behalf. A module type can be referred to by name, but its ultimate definition
takes one of three forms:
• An export list, written using exports, which lists the module’s components,
to which it promises access
• A modulearrow type, written using the module arrow ‑‑m‑>, which describes
the parameters and the result type of a generic module
Export lists
• In an export list, before a type component can be used to give the type of a
value component, the type component must be declared.
• Within a single export list, no component may be declared more than once.
• The types used in every declaration must be well formed.
A module type only makes promises—it doesn’t deliver any values, types, or mod‐
ules. To deliver on the promises requires a module, which is defined using one
of the module forms from Figure 9.4. An exporting module is typically defined
by giving its name M , its module type T , and a sequence of definitions, like so:
(module [M : T ] d1 · · · dn ). The definition of M is well typed only if all the di ’s
are well typed and if M provides all the components listed in T . And the definition
seals module M with type T . Sealing limits access to the components defined by
definitions d1 · · · dn : outside of M , the only components that can be named are
those mentioned in T . And if any of the type components is declared as an abstract
type, its definition is hidden.
Exporting modules can also be defined using two other forms. First, an exist‐
ing module M ′ may be sealed using the form (module [M : T ] M ′ ). New mod‐
ule M has the same components as existing module M ′ , but only components
exported by module type T are visible. And if any type component t is declared
as abstract in T , then sealing gives type M.t a new identity that is distinct from
M ′ .t. (In the language of Chapter 8, sealing is generative.) Second, an existing
module may be abbreviated, without sealing, using the form (module M M ′ ), as
in (module IR (@m Ref Int)). Such abbreviations are used primarily for instances
of generic modules, which are discussed below.
Module elimination: Qualified names
Client code can observe or interrogate a module in only one way: it can name
a component. The component’s qualified name is formed by concatenating the
9 name of the module, a dot, and the name of the component. Qualified names are
used in many languages, including Ada, CLU, Haskell, Java, OCaml, Modula‐3, and
Standard ML. In Molecule, as in most of these languages, a qualified name like
Molecule, abstract
IntArray.at selects a component from a module. Qualified names can also in‐
data types, and
stantiate generic modules, as described below.
modules
A good module can embody a lot of craft—think about the engineering that goes into
a balanced search tree or a good hash table. Such engineering should be reusable,
and you already know a suitable mechanism: higher‐order, polymorphic functions.
But at this scale, lambda and type‑lambda are awkward; they work with individual
values and types, and the unit of reuse should be the module. To support polymor‐
phism with modules, Molecule provides generic modules.
A generic module takes one or more other modules as formal parameters, and it
produces a module as its result. By taking modules as parameters, generic modules
provide, in one mechanism, the capabilities of higher‐order functions (functions as
parameters, as with lambda) and parametric polymorphism (types as parameters,
as with type‑lambda).
A parameter of a generic module may not itself be generic. This sort of restric‐
tion, which says that the parameter to a polymorphic thing may not itself be poly‐
morphic, makes the polymorphism predicative. Predicativity can simplify a type
theory considerably, so predicative polymorphism is common. For example, the
Hindley‐Milner type system used in core ML is predicative, and so are systems for
other languages that support generic modules, including CLU, Ada, and Modula–3.
Type systems in which type parameters are not restricted, like Typed µScheme, are
called impredicative.
In Molecule, the type of a generic module is written like a function type, except
that each formal parameter gets a name (so that later parameters and the result type
may refer to it), and the parameters are separated from the result type by the mod‐
ule arrow ‑‑m‑>. For example, the type of Molecule’s predefined, generic Array
module is as follows:
540a. hpredefined Molecule types, functions, and modules 529ai+≡ ◁ 529b 541b ▷
(module‑type GENERIC‑ARRAY
([Elem : (exports [abstype t])] ‑‑m‑>
(exports [abstype t] ;; an array
[type elem Elem.t] ;; one element of the array
[new : (int elem ‑> t)]
[empty : ( ‑> t)]
[size : (t ‑> int)]
[at : (t int ‑> elem)]
[at‑put : (t int elem ‑> unit)])))
The elem type in the result module is known to be equal to the type t in the param‐
eter module, Elem. That crucial identity can be expressed only because the formal
parameter is named.
Module type GENERIC‑ARRAY is the type of the primitive module Array:
540b. hinterface checks of predefined modules 530ai+≡ ◁ 530a
(check‑module‑type Array GENERIC‑ARRAY)
A module‐arrow type is well formed only if the module type of each formal
parameter describes an exporting module, not a generic module—that’s the pred‐
icative polymorphism. The module type of each parameter must be well formed,
and the module type of the result must also be well formed. The module type of
the result may refer to formal parameters, like Elem in the example above. And the
module types of later parameters may refer to earlier parameters.1
A generic module is introduced using the same definition form as an exporting
§9.4
module, except that it uses the keyword generic‑module, and the module type used
The Molecule
to seal it must be a module‐arrow type. For an example, see the definition of generic
language
module ArrayHeap in chunk 552a.
A generic module is eliminated by instantiating it. Instantiation creates an 541
exporting instance of a generic module. The concrete syntax resembles the in‐
stantiation of a polymorphic value in Typed µScheme, but to remind us of the
distinction between a generic module in Molecule and a polymorphic value in
Typed µScheme, a generic instantiation is written using keyword @m. For example,
the predefined module String is an instance of the generic Array module:
541a. hdefinition of module String 541ai≡
(module String (@m Array Char))
Intersection types
9
Figure 9.7: Names that are overloaded in Molecule’s initial basis
Molecule, abstract
data types, and
modules it selects a named component from a module record. When a generic‑module
definition form is evaluated, it produces a function whose arguments are module
542 records and whose result is a module record. When a generic module is instanti‐
ated, the function is applied.
A pitfall
In Molecule, the function that a generic module evaluates to can be impure. For ex‐
ample, if a generic module includes mutable state—say it mutates a private variable
defined with val—each instance gets fresh mutable state, which isn’t shared with
other instances. This behavior might cause confusing results, especially if a ge‐
neric module is instantiated by the type checker during the resolution of an over‐
loaded operator. So don’t mutate variables that are defined inside generic modules.
expands to
(exports [abstype t]
[make : (Int.t Bool.t ‑> t)]
[n : (t ‑> Int.t)]
[b : (t ‑> Bool.t)]
[set‑n! : (t Int.t ‑> Unit.t)]
[set‑b! : (t Bool.t ‑> Unit.t)])
When every operation needs a qualified name, code can feel bloated and tedious;
unqualified names like +, print, and = are easier on the eyes. To enable unqual‐
ified names to be used in more contexts, Molecule allows the name of a function
to be overloaded. An overloaded name may stand for more than one value or func‐
tion; to determine which function is meant, Molecule looks at the type of its first
§9.4
argument.
The Molecule
Overloading helps answer three questions that the designers of any statically
language
typed programming language ought to address:
543
1. Programmers like to use + for both integer addition and floating‐point addi‐
tion. How is the language to know which is meant, when?
After these definitions, the unqualified form of each function name is now over‐
loaded. To see what an unqualified name might stand for, ask the interpreter:
543b. htranscript 530bi+≡ ◁ 542 543c ▷
‑> =
overloaded = : (Char.t Char.t ‑> Bool.t)
= : (Sym.t Sym.t ‑> Bool.t)
= : (Bool.t Bool.t ‑> Bool.t)
= : (Int.t Int.t ‑> Bool.t)
Each use of an overloaded name is associated with a single function during type
checking; the process is called overload resolution. In Molecule, overload resolution
uses the simplest algorithm that I could find, which is based on CLU: an overloaded
name may be used only as a function that is applied to one or more arguments, and
the meaning of the overloaded name is resolved by looking at the type of the first
argument. The formalities appear in Section 9.8.8.
Overloading is sometimes called ad hoc polymorphism. I don’t care for this
term; overloading resembles parametric polymorphism and other forms of poly‐
morphism a little too superficially for my taste. But the term is used in some other
books and in some important and interesting papers.
Interesting abstract types require the tools needed to define interesting represen‐
tations. In addition to algebraic data types and record modules, Molecule provides
our customary primitive representations, plus several array modules.
Primitive types are defined in modules Int, Bool, Unit, and Sym. Their main
types are abbreviated int, bool, unit, and sym. They are supplemented by prede‐
fined functions and, or, not, and mod, by value constructors #t, #f, and unit, and
by the overloaded names listed in Figure 9.7 on page 542.
Molecule includes a predefined Char module, which is not primitive; a value
of type Char.t is represented by its Unicode code point (an integer), and module
Char is a client of module Int. In addition to function new, which takes an integer
designating a Unicode code point and returns the corresponding character, mod‐
ule Char exports characters left‑curly, right‑curly, left‑round, right‑round,
left‑square, right‑square, newline, space, semicolon, and quote.
To enable a compact way of implementing comparisons, Molecule includes a
predefined module Order, whose exported type Order.t has value constructors
LESS, EQUAL, and GREATER.
Molecule provides several array abstractions, including ArrayList, which can
grow and shrink at either end. All array abstractions provide constant‐time access
to elements. Arrays are supported by modules UnsafeArray, ArrayCore, Array,
IntArray, and ArrayList.
Predefined module types include ARRAY, GENERIC‑ARRAY, and ARRAYLIST.
Molecule also provides a generic Ref module, with operations new, !, and :=.
References work as they do in ML.
9.6 PROGRAM DEſıGN: ABſTRACTıONſ
• When designing an interface, you must say what an abstraction is, whether
it is mutable, what operations are supported, and what they cost. And you §9.6
must plan for a representation that can do the job. Program design:
Abstractions
Once an interface is designed, you can classify each operation as a creator,
producer, mutator, or observer (page 111). The classification can help you con‐ 545
firm that the interface is not missing anything obvious and that its operations
will work well together.
These techniques apply equally well to program design with objects (Chapter 10),
but in this chapter they are illustrated with modules and abstract data types. De‐
scription and classification are illustrated with array lists and sets; abstraction
functions are illustrated with multiple examples, and representation invariants are
illustrated with priority queues, both here and in Section 9.7.
Every designer must decide whether an abstraction will be mutable, what opera‐
tions an interface will provide, and what those operations might cost. All three
considerations influence (and are influenced by) the intended representation.
If an abstraction has a state that can change over time, it’s mutable. Otherwise,
it’s immutable. A mutable abstraction has a mutable representation. An immutable
abstraction usually has an immutable representation, but it can have a mutable
representation—a classic example is an immutable data structure with a mutable
cache.
Mutability isn’t arbitrary; for example, atomic values—like integers, Booleans,
characters, and enumeration literals—are expected to be immutable. Aggregate
values like strings, arrays, and records, which store other values inside, are of‐
ten mutable—though in functional languages, strings and records are typically im‐
mutable, while arrays typically appear in both mutable and immutable forms.
Mutable representations normally require less allocation than immutable rep‐
resentations and so can be less expensive; for example, a mutable binary search
tree can be updated with at most constant allocation. But a mutable representation
demands a mutable abstraction, and compared with immutable abstractions, mu‐
table abstractions are harder to test and can lead to more bugs. In particular, when
mutable data is shared among different parts of a program, one part can make a
change that another wasn’t expecting. (“Not found? But that key was in the table!”)
With immutable abstractions, these sorts of bugs can’t happen.
General‐purpose abstractions should often be designed in both mutable and im‐
mutable forms. For example, if I’m using an association list to represent an envi‐
ronment in an interpreter, I want an immutable alist—that’s going to make it easy
to implement let expressions and closures. But if I’m using an association list to
implement a sparse array, I want a mutable alist—that’s going to reduce the alloca‐
tion cost of an update from linear to constant. If you design mutable and immutable
abstractions in tandem, you’ll always have the one you want when you want it.
Abstractions can be designed in tandem in part because mutability is surpris‐
9 ingly independent of the designer’s other big choice: what operations to provide.
Mutability rarely affects what operations are implemented; for example, whether
it is mutable or immutable, a dictionary abstraction needs to implement the same
Molecule, abstract
operations: insert, lookup, update, and delete. The same is true of a stack, a pri‐
data types, and
ority queue, an array, and many other abstractions. Mutability does affect costs;
modules
the cost of each operation, called the cost model, often depends both on mutability
546 and on the representation the designer has in mind.
A cost model determines what operations will be cheap, what operations will be
expensive, and what operations won’t be implemented at all. Identifying the right
operations at the right cost calls for techniques from beyond the world of program‐
ming languages. But once you have a set of operations, programming‐language
techniques can help you see if it’s a good one: You can analyze the operations ac‐
cording to their classification (Chapter 2, page 111), which is closely related to clas‐
sification of rules in type theory (sidebar, page 347).
• Every interface needs an operation that creates a new value of abstract type:
a creator.
• Most interfaces need operations that can take an existing value of abstract
type and do something with it. An operation that updates a value in place is
a mutator; one that uses an existing value to make a new value is a producer.
One example is insertion into a dictionary: a mutable dictionary would im‐
plement insertion as a mutator, and an immutable dictionary would imple‐
ment it as a producer.
• Every interface needs operations that get information out of a value of ab‐
stract type: observers. As examples, observers in my digital video recorder
tell me whether a game has started, whether it’s over, and who’s playing.
A design with these properties probably meets clients’ needs—and can be tested
without having to violate abstraction.
Once you’ve chosen your abstraction and its operations, it’s time to write the com‐
plete API. The abstraction is central: it dictates the language you use to specify the
behavior of each operation. When precision is important, the abstraction and the
language should be mathematical, so operations can be described with mathemat‐
ical precision. As an example, I present what Java calls an ArrayList: a mutable
sequence that provides constant‐time indexing but can also grow and shrink.
The abstraction is a sequence of elements, each of type elem. The elements are
numbered sequentially, and the number of the first element is part of the abstrac‐
tion. I write the abstraction as hk, vsi, where vs is a sequence of values and k is the
index of the first value.
The abstraction looks like an array, but it can grow or shrink at either end, in
constant amortized time. I plan a single creator, operation from, which takes one
argument k and returns hk, [ ]i, the empty array starting at k . I don’t plan any pro‐
ducers; rather than produce a new array from an existing array, I plan to update
arrays in place.
Update is implemented by mutator at‑put. Growth and shrinkage are imple‐
mented by mutators addlo, addhi, remlo, and remhi. Individual elements are ob‐
§9.6
served by at, and the size and bounds of the array are observed by size, lo, and
Program design:
nexthi.
Abstractions
547. harraylist.mcl 547i≡ (S486d)
(module‑type ARRAYLIST 547
(exports [abstype t]
[abstype elem]
[from : (int ‑> t)] ; creator (from initial index)
[size : (t ‑> int)] ; observer
[at : (t int ‑> elem)] ; observer
[at‑put : (t int elem ‑> unit)] ; mutator
Such laws can easily specify the behavior or creators, producers, and observers.
Specifying mutators requires more work. The best simple specification relates the
two states of the abstraction before and after the mutation; an array A would have
states Apre and Apost . For example,
This design is complete: Index k can be observed using lo, and all of vs can be
observed by using at with an index i in the range lo ≤ i < nexthi. Every at‑put
can be observed by an at. Effects of addlo and remlo can be observed by lo, size,
and at. Similarly for addhi and remhi.
The design is implemented in the Supplement, as module ArrayList.
Abstraction Operations
Set At least empty/new, insert, delete, member?; pos‐
sibly also empty?, size, union, inter, diff
9 Representation Invariant
Array No element is repeated.
Molecule, abstract List No element is repeated.
data types, and Sorted list No element is repeated; elements are sorted.
modules Binary search tree No element is repeated; smaller elements are in
left subtrees; larger elements are in right subtrees;
548 perhaps some sort of balance invariant.
Abstraction functions
The abstraction function combines all elements of the representation. A function
for a binary search tree is specified in the text on the facing page; for a list, the laws
of the abstraction function are A('()) = { } and A(cons v vs ) = {v} ∪ A(vs).
9.6.3 Case study in specification using only algebraic laws: An immutable set
(member? v empty) = #f
(member? v (insert v s)) = #t
(member? v ′ (insert v s)) = (member? v ′ s), where v 6= v ′
(empty? empty) = #t
(empty? (insert v s)) = #f
(delete v empty) = empty
(delete v (insert v s)) = (delete v s)
(delete v (insert v ′ s)) = (insert v ′ (delete v s)), where v =
6 v′
Observers member? and empty? are applied to sets made with empty and insert. Re‐
sults from delete are specified by rewriting delete away: by applying the delete
laws repeatedly from left to right, every set made using delete is eventually shown
to be equal to a set made with only empty and insert.
These algebraic laws are so simple that I feel no need to specify the operations
informally. Try writing such laws for yourself (Exercise 15)!
Abstraction Operations
Dictionary At least empty/new, insert/bind, delete, lookup
or find; possibly also empty?, size, and others
Representation Invariant
Association list Every key is paired with the value it maps to.
Hash table Every key‐value pair is stored in a bucket in an ar‐ §9.6
ray, the index of which is a function of the array’s Program design:
size and the element’s integer hash. Abstractions
Binary search tree Pairs with smaller keys are in left subtrees; pairs 549
with larger keys are in right subtrees.
Abstraction functions
To specify an abstraction function, treat the dictionary abstraction as an environ‐
ment (a set of key‐value pairs), and use the + operation defined on page 149. For
example, the abstraction function for an association list is defined by these laws:
A('()) = {}
A((cons (pair k v ) ps )) = A(ps) + {k 7→ v}
A(Empty) = { },
A(Node l v r) = A(l) ∪ A(r) ∪ {v}.
Abstraction Operations
Priority queue At least empty/new, insert, empty?, and
delete‑min; possibly also size, find‑min, merge
9 Representation Invariant
List List is sorted with the smallest element at the front
Molecule, abstract (inefficient unless small).
data types, and Array Element at index i is not larger than the elements
modules at indices 2i and 2i + 1, if any.
550 Binary tree Element at node is not larger than elements at left
and right child, if any.
Leftist heap Binary tree, with the additional invariant that ev‐
ery left subtree is at least as high as the corre‐
sponding right subtree.
Abstraction functions
The abstraction function for a priority queue has the same structure as the abstrac‐
tion function for a set. For example, the abstraction function for a leftist heap says
A(Empty) = H I and A(Node l v r) = A(l) ∪ A(r) ∪ HvI.
For the Cartesian representation, the abstraction function is the identity function.
For the polar representation, it is A(r, θ) = (r cos θ, r sin θ).
Abstraction Operations
Complex number Constructor, +, ‑, *, /, negated
Representation Invariant
Cartesian coordinates (x, y) None
Polar coordinates (r, θ) r ≥ 0, possibly −π ≤ θ < π
Calling insert or delete‑min mutates the queue that is passed in, and calling new
creates a fresh, empty queue that is distinct from all other queues. Both insert
and delete‑min run in time logarithmic in the size of the queue, and the other
operations run in constant time.
My implementation is generic: it works with any type of element, provided
only that the smaller of any two elements can be identified. My generic module
9 ArrayHeap therefore takes one argument, module Elem, which exports type Elem.t
and function Elem.<=. Function Elem.<= must behave like a total order: it must be
reflexive, transitive, and antisymmetric. To use ArrayHeap with any other <= func‐
Molecule, abstract
tion is an unchecked run‐time error.
data types, and
modules 552a. hpq.mcl 551i+≡ ◁ 551
(generic‑module
552 [ArrayHeap : ([Elem : (exports [abstype t]
[<= : (t t ‑> bool)])] ‑‑m‑>
(allof MUTABLE‑PQ
(exports [type elem Elem.t])))]
hrepresentation of a priority queue 552bi
hdefinitions of operations on priority queues 553ai
)
The result type of generic module ArrayHeap uses allof in the same way as
IntArray (chunk 541b), for the same reason: it needs to let client code know that
in any instance of ArrayHeap, type elem is the same as type Elem.t.
My implementation uses the classic representation of a mutable heap: an array.
552b. hrepresentation of a priority queue 552bi≡ (552a)
(module EA (@m ArrayList Elem)) ; EA stands for "element array"
(type t EA.t)
(type elem Elem.t)
The array represents a complete binary tree in which the root is stored at index 1
and the two children of the node at index i are the nodes at indices 2 · i and 2 · i + 1.
My array A is actually the “array list” whose interface is described in Section 9.6.2,
so in addition to its elements, it has integers lo and nexthi , which mark the ex‐
treme indices of the array. A representation A represents the bag containing all
the elements of A:
A(A) = HA[i] | 1 ≤ i < nexthi I.
A good representation satisfies these invariants:
These invariants help me guarantee that both insert and delete‑min execute in
time proportional to the logarithm of the number of elements in A.
To ensure that the invariants hold, I use the classic method of relyguarantee
reasoning:
• At the start of every operation, my code relies on every value of abstract type
having a representation that satisfies the invariants.
The invariant is satisfied if lo = 1 and all children are good. To test it, I define
higher‐order function all‑in‑range?, which tests all integers in a given range.
553b. hdefinitions of operations on priority queues 553ai+≡ (552a) ◁ 553a 553c ▷
(define bool all‑in‑range? ([i : int] [limit : int] [p? : (int ‑> bool)])
;; true iff for every k such that i <= k < limit, k satisfies p?
(|| (>= i limit)
(&& (p? i) (all‑in‑range? (+ i 1) limit p?))))
The invariant is exploited for both insertion and deletion, and insertion is a lit‐
tle easier to understand. Function insert adds a new element at the high end of
the array, in position n. Now every position i satisfies the invariant, except possi‐
bly position n. If A[n div 2] ≤ A[n], then position n satisfies the invariant. Oth‐
erwise insert swaps A[n] with A[n div 2], then tries again with a new value of n.
Eventually either A[n div 2] ≤ A[n] or else n = 1, and either way the invariant is
eventually satisfied.
553d. hdefinitions of operations on priority queues 553ai+≡ (552a) ◁ 553c 554a ▷
(define unit insert ([v : Elem.t] [q : t])
(let ([n (EA.nexthi (validate q))])
(EA.addhi q v)
; loop invariant: (EA.at q n) == v
(while (&& (> n 1) (not (Elem.<= (EA.at q (/ n 2)) v)))
(EA.at‑put q n (EA.at q (/ n 2)))
(EA.at‑put q (/ n 2) v)
(set n (/ n 2)))
(assert (invariant? q))))
Deletion is a little more complicated. The invariant ensures that the smallest
element is always stored in A[1], so function delete‑min removes A[1], then moves
in the last element of A, if any. At that point the invariant could be violated in this
way: A[1] could be larger than its children, A[2] and A[3]. To restore the invariant,
delete‑min swaps A[1] with the smaller of A[2] and A[3]. Now A[1] satisfies the
invariant, but the child that was swapped may not. That child is at index 2 or 3, but
the computation generalizes to A[i], A[2 · i], and A[2 · i + 1]. Function delete‑min
keeps swapping, and i keeps growing, and this computation continues until either
the relations are satisfied or A[i] has no children.
9 554a. hdefinitions of operations on priority queues 553ai+≡
hdefinition of internal operation swap 554ci
(552a) ◁ 553d 554d ▷
The priority queue can be used, among other things, for sorting (Exercise 40).
9.7 KEY FEATURE: INſPECTıNG MULTıPLE REPREſENTATıONſ
The priority queue in the previous section—a complete binary tree represented by
an array—provides insertion and removal in O(log N ) time, but it does not provide
a merge operation. To merge two priority queues you would have to remove all
the elements from one and add them to the other, which would cost O(N log N ).
In this section, I present an immutable priority‐queue abstraction with an efficient
merge operation: the leftist heap. The merge operation can be implemented effi‐
ciently only because it can inspect the representation of both heaps.
An immutable priority queue exports roughly the same operations as a mutable
priority queue, but the operations have different types:
555a. hleftist.mcl 555ai≡ 555b ▷
(module‑type IMMUTABLE‑PQ
[exports [abstype elem]
[abstype t]
[empty : t] ; creator
[insert : (elem t ‑> t)] ; producer
[merge : (t t ‑> t)] ; producer
[empty? : (t ‑> bool)] ; observer
[module [Pair : (exports‑record‑ops pair
([min : elem] [others : t]))]]
[delete‑min : (t ‑> Pair.pair)]]) ; observer/producer
Operation delete‑min returns a pair holding min, the smallest element, and the
others; the pair type is defined internally in nested module Pair.
Module Leftist uses allof in the same way as IntArray and ArrayHeap.
555b. hleftist.mcl 555ai+≡ ◁ 555a
(generic‑module
[Leftist : ([Elem : (exports [abstype t]
[<= : (t t ‑> bool)])] ‑‑m‑>
(allof IMMUTABLE‑PQ
(exports [type elem Elem.t])))]
hrepresentation of a priority queue as a leftist heap 556ai
hdefinitions of operations on leftist heaps 556bi
)
The representation and operations are shown on the next couple of pages.
A leftist heap is a binary tree in which every node satisfies two invariants:
• The heap invariant says that the value stored at the node is no greater than
any value stored in either of its subtrees.
9 • The pathlength invariant says that if path lengths are measured from the
node to the leaves, the shortest path is on the right.
Molecule, abstract The path‐length invariant needs to be made precise, but the imprecise version ex‐
data types, and plains why the representation is called “leftist”: longer paths are on the left, so the
modules left subtree tends to have more nodes.
To make the path‐length invariant precise, I define the rank of a tree as the
556 length of the shortest path from the root to a leaf. Rank is defined inductively:
the empty tree has rank zero, and a nonempty tree has rank one more than the
minimum rank of both subtrees. The path‐length invariant says that in any non‐
empty node of a leftist representation, the rank of the left subtree is at least as great
as the rank of the right subtree. To maintain the invariant, when I make a new node
I put the higher‐rank subtree on the left.
Rank can be computed by visiting every node, but that’s costly. Instead, each
node’s rank is cached in the node itself, making the rank available in constant time.
Because the representation is immutable, rank needs to be computed only once,
when the node is built—afterward, the cached rank is always up to date. The binary
tree is represented using an algebraic data type; rank is cached only in a nonempty
tree.
556a. hrepresentation of a priority queue as a leftist heap 556ai≡ (555b)
(type elem Elem.t)
(data t
[LEAF : t]
[NODE : (Elem.t t t int ‑> t)]) ; value, left and right subheaps, rank
Thanks to the cache, the rank of any tree is available in constant time:
556b. hdefinitions of operations on leftist heaps 556bi≡ (555b) 556c ▷
(define int rank ([heap : t])
(case heap
[LEAF 0]
[(NODE _ _ _ n) n]))
This optimized merge is possible only because merge can inspect the representa‐
tions of both arguments, h1 and h2.
The other operations are left to you (Exercise 33).
To get experience writing functions that can inspect all of their arguments, try im‐
plementing arithmetic. A function that adds two numbers, for example, needs ac‐
cess to every digit of both numbers. By implementing arithmetic on so‐called large
integers, where there is no a priori limit to the number of digits, you can compare the
abstract‐type approach to data abstraction (this chapter) with the object‐oriented
approach (Chapter 10). Arithmetic was once every schoolchild’s introduction to al‐
gorithms, but if you have forgotten the classic algorithms used to add, subtract, and
multiply numbers of many digits, they are explained in detail in Appendix B.
Implementing arithmetic will give you insight into similarities and differences
between abstract data types and objects. Abstract data types and objects use the
same representation (“sequence of digits”), and they use data abstraction to protect
the same invariants (“the leading digit of a nonzero number is never zero”), but
from there, they diverge:
• In Exercise 49 in this chapter, you can define a function that adds two natural
numbers. Because it is defined in the same module as the representation of
natural numbers, this function can simply look at the digits of both represen‐
tations, add them pairwise, and return a result. But this function is limited
to the type on which it is defined; it cannot dream of, for example, adding a
natural number to a machine integer to get a natural‐number result. If client
code wants to add a natural number and a machine integer, it must first co
erce the machine integer to a natural number. (Such a coercion is a key step
in the algorithm for multiplying two natural numbers.)
• By contrast, an addition method defined on an object can see only the digits
of the addend on which it is defined; it must treat the other addend as an ab‐
straction. To make addition possible requires changing the abstraction’s API,
so it can include operations like “tell me your least‐significant digit.” But be
cause the other addend is an abstraction, it doesn’t have to have the same rep‐
resentation as the first addend—and in Exercises 37 to 39 in Chapter 10, you
can build an object‐oriented implementation of arithmetic that can seam‐
lessly add machine integers and large integers. Using objects, client code
can simply add numbers and not worry about their types.
Large integers make a great case study for comparing abstract data types with ob‐
jects, but they are also a vital abstraction in their own right—integers of practically
unlimited size are supported natively in many programming languages, including
Icon, Haskell, Python, and full Scheme.
9.8 MOLECULE’ſ TYPE ſYſTEM: ENFORCıNG ABſTRACTıON
The type system’s main job is to make modules and abstract types work:
• When a module is sealed, the type system hides the representations of its
abstract types, and it gives each abstract type a new identity.
• When a generic module is defined, the type system checks it right away, as‐
suming that each parameter has the module type claimed for it. When the
module is instantiated, the type system checks that each actual parameter
implements the corresponding module type, but it needn’t check the generic
code again—if the actual parameters are OK, the instance is guaranteed to be
well typed. This ability to check a generic module once, rather than have to
check each instance, is an example of modular type checking.
Although type Char.t is defined as int, this identity is known only inside the Char
module. On the outside, type Char.t has a new identity that is distinct from int,
and it is known only by that identity. So a value of type Char.t won’t work with
integer operations:
558b. htranscript 530bi+≡ ◁ 543c 559a ▷
‑> (+ 1 Char.right‑curly)
type error: function + expects second argument of type Int.t, but got Char.t
‑> (= 125 Char.right‑curly)
type error: function = expects second argument of type Int.t, but got Char.t
Char.right‑curly does not work with operations exported from Int; it works only
with the operations exported from Char (print and println).
As an example of identity preservation, I copy module Char into module C. Be‐
cause copying the module doesn’t change its types, type C.t is the same type as
Char.t. And the right‑curly value from module Char does work with the println
operation from module C:
559a. htranscript 530bi+≡ ◁ 558b 559b ▷
‑> (module C Char)
§9.8
‑> (C.println Char.right‑curly)
Molecule’s type
}
system: Enforcing
unit : Unit.t
abstraction
The identity of type Char.t is preserved by a transformation called strengthening:
when Char is used on the right‐hand side, all uses of its abstract type t are replaced 559
with the fully qualified name Char.t. In addition, the module type is changed so
that type t is no longer abstract; instead, t is made manifestly equal to itself:
559b. htranscript 530bi+≡ ◁ 559a 559c ▷
‑> (module C Char)
module C :
(exports
[type t Char.t]
[new : (Int.t ‑> Char.t)]
...
A new, unique root is created for every new module, regardless of what the module
is named—that is, module‐definition forms are generative (Sections 8.5 and 9.8.9).
Because every root is unique, or has a unique root substituted for it when used,
every absolute access path is also unique.
Giving every type a unique absolute access path is the key to the type sys‐
tem: in Molecule, two types are equal if and only if they are identified with the same
path (or if they are function types mapping equal argument types to equal result
types). Type equality is used to ensure that each component of a module meets
its specification—that is, if a module type includes a value component, the module
must define that component, and the component must have the expected type.
When sealed, an entire module is also checked to see if it meets its specification.
For example, module IntArray cannot be sealed with a module type that specifies
“array of Booleans”:
559c. htranscript 530bi+≡ ◁ 559b 560a ▷
‑> (module‑type BOOLARRAY (allof ARRAY (exports [type elem Bool.t])))
‑> (module [MyArray : BOOLARRAY] IntArray)
type error: interface calls for type elem to manifestly equal Bool.t,
but it is Int.t
Because IntArray does not implement BOOLARRAY—the element type is wrong—this
sealing doesn’t typecheck. But sealing IntArray with a module type that specifies
“array of integers” is just fine:
560a. htranscript 530bi+≡ ◁ 559c 560b ▷
9 ‑> (module‑type INTARRAY (allof ARRAY (exports [type elem Int.t])))
‑> (module [MyArray : INTARRAY] IntArray)
module MyArray :
Molecule, abstract
(exports
data types, and [abstype t]
modules [type elem Int.t]
[new : (Int.t Int.t ‑> MyArray.t)]
560
[empty : ( ‑> MyArray.t)]
[size : (MyArray.t ‑> Int.t)]
[at : (MyArray.t Int.t ‑> Int.t)]
[at‑put : (MyArray.t Int.t Int.t ‑> Unit.t)])
The type system checks that IntArray implements INTARRAY, and this sealing is
accepted.
As another example, even though an integer heap made with ArrayHeap has an
array representation, it cannot be sealed with an ARRAY specification:
560b. htranscript 530bi+≡ ◁ 560a 560c ▷
‑> (module [IntHeap : ARRAY] (@m ArrayHeap Int))
type error: interface calls for value new to have type ...
but it has type (‑> (@m ArrayHeap Int).t)
The check fails because the integer heap’s new operation has a type that is different
from what the ARRAY interface specifies. The types of all the operations can be seen
by giving the instance a name (IntHeap) without any sealing:
560c. htranscript 530bi+≡ ◁ 560b 560d ▷
‑> (module IntHeap (@m ArrayHeap Int))
module IntHeap :
(exports
[type t (@m ArrayHeap Int).t]
[type elem Int.t]
[new : ( ‑> (@m ArrayHeap Int).t)]
[insert : (Int.t (@m ArrayHeap Int).t ‑> Unit.t)]
[empty? : ((@m ArrayHeap Int).t ‑> Bool.t)]
[delete‑min : ((@m ArrayHeap Int).t ‑> Int.t)])
When a module is sealed, the type system checks that the module implements
the interface used to seal it. And similarly, when a generic module is instantiated,
the type system checks that each module argument implements the interface of the
corresponding formal parameter. For example, the ArrayHeap module requires a
formal parameter whose interface exports a type t and a <= operation. Module
Int implements that interface, but Bool doesn’t, so ArrayHeap can’t be instantiated
with Bool:
560d. htranscript 530bi+≡ ◁ 560c 560e ▷
‑> (module BoolHeap (@m ArrayHeap Bool))
type error: module Bool cannot be used as argument Elem to generic module ...
The directions of “subtype” and “supertype” always confuse me. I think of the
“sub” in “subtype” and think it means “fewer,” but actually a subtype has more
components. I correct my thinking by asking how many inhabitants a module
type has. A subtype has fewer inhabitants—that is, there are fewer modules that §9.8
redeem the promise made by the type. In fact the inhabitants of a subtype are Molecule’s type
a subset of the inhabitants of the supertype—which is why an inhabitant of the system: Enforcing
subtype may be used anywhere an inhabitant of the supertype is expected. abstraction
561
Molecule’s type system uses techniques that should be familiar from Chapters 6
and 8, but at a larger scale. Its elements are shown in Figure 9.13 on the following
page, which depicts the major metavariables, the syntax, and the environment.
The type system is organized around access paths; a path is written π . A path
may be a bare module name M , or it may be formed by selecting a value, type, or
module component from a shorter path. Or a path may be an instance of a generic
module, or finally, a path may be just •, which is a placeholder used to describe a
module type that is not yet part of a module definition, like the module type ARRAY.
The typing rules are written using a compact representation of syntax (Fig‐
ure 9.13e). Because the type system is so large and because declarations and com‐
ponents are so similar, the metavariable D stands not only for a declaration as
written externally in the source code but also for a component of a module used
internally.
(a) Names (b) Syntax, environments
9 x or f
M
K
Name of a variable (or function)
Name of a module
Name of a value constructor
d
ds
Definition
List of definitions
τ Type
Molecule, abstract T Module type
data types, and (c) Declarations: Syntax and theory D Declaration or component
modules Ds List of declarations or components
[abstype t] t :: ∗ or t :: b∗cπ
562 [type t τ ] t=τ C Context of a definition
[x : τ ] x:τ E Static (typechecking) environment
[module [M : T ]] M :T ρ Dynamic (evaluation) environment
D ::= t = τ t :: ∗ x : τ M : T
C ::= [ ] π .[ ]
τ ::= π τ1 × · · · × τn → τ
E ::= [ ] E, t = τ E, x : τ E, M : bT cπ E, T = T E, x ∈ [τ1 , . . . , τn ]
Figure 9.13c clarifies the meaning of the declaration forms by relating concrete
syntax to theory notation. Each form of theory notation expresses a type‐theoretic
idea: abstract type t has kind ∗; manifest type t is equal to τ ; and so on. As shown
in the figure, abstract‐type declarations have two theory forms: The external form
simply identifies an abstract type t, and it is notated t :: ∗. The internal form is
decorated with its absolute access path π , and it is notated t :: b∗cπ .
To compute the absolute access path of each type and module, the type system
tracks the context of each declaration and definition. The form of a context C is
shown in Figure 9.13e. A context has a hole [ ], which is filled in with a name, like
the name of a defined module. Filling the hole turns the context into a path. Each
top‐level definition is elaborated in the context [ ], which is just a hole. The name
used to fill it is the name of the module being defined.
A type τ is either an absolute access path or a function type.
Abstract syntax includes lists of definitions ds and of declarations Ds . In this
chapter, lists are written differently than in other chapters: a nonempty list is
formed using an infix comma, which can mean not only cons but also append or
§9.8
snoc (which adds a single element to the end of a list). The comma is also used to
Molecule’s type
pattern match on nonempty lists. This notation simplifies the typing rules.
system: Enforcing
Compared with type systems that appear in earlier chapters, Molecule’s dif‐
abstraction
fers most in its type‐checking environment. Molecule’s type‐checking environ‐
ment does the same work as Typed µScheme’s Γ and ∆ environments combined: 563
it tracks the type of each value and the kind of each type constructor.2 But it also
tracks the type that each type abbreviation stands for, the module type of each
module, the module type that each module‐type abbreviation stands for, and the
possible types of each overloaded name. This information could conceivably be
distributed over multiple different environments, much as types and kinds are dis‐
tributed over environments Γ and ∆ in Typed µScheme, but putting it all into a
single environment E simplifies the forms of many judgments. One consequence
is that Molecule cannot simultaneously have both a value and a type of the same
name, but this restriction is one I can live with.
A type‐checking environment E is written as a sequence of bindings. A bind‐
ing resembles a declaration, and both bindings and declarations are notated with
metavariable D . But a binding differs from a declaration in the following ways:
• When a binding associates a module’s name M with its module type T , the
module type is rooted in path π ; the binding is written M : bT cπ , where π is
M ’s absolute access path. Both type and path are needed because M can be
used in two ways: When M is used to define another module or as an actual
parameter to a generic module, its type T is needed for type checking. And
when M is used to form a path—that is, when code names a type component
like M.t—M ’s absolute path π is used to form path π.t, which determines
the identity of the type.
Like an allof type, a type of the form T1 ∧ T2 is an intersection type; the ∧ symbol
can be pronounced “and also.”
The second simplification is for overload: although the concrete syntax may
list as many paths as you like, the type theory overloads one path at a time.
An overview of the entire type system is shown in Figure 9.14 on the next page.
While the figure shows a lot of judgments, only three things are really going on:
elaboration, principal types, and subtyping. In this order,
• Types and module types are elaborated, which replaces each type abbrevia‐
tion with its referent. Elaboration also replaces relative paths with absolute
paths as needed. And although it is mostly glossed over, elaboration dec‐
orates the application of every overloaded function name, so the evaluator
knows which overloaded function is meant.
Checking ascriptions is the ultimate goal, so we begin our study with subtyping,
then work through principal module types and elaboration.
subtypes of intersection types gets at the very idea of subtyping and intersection:
a module inhabits type T1′ ∧ T2′ if and only if it inhabits both T1′ and T2′ . The rule
for subtypes of an intersection type is therefore sound and complete. (If you like
math, subtyping is a partial order, and the ∧ operator finds a greatest lower bound.)
The rule for a subtype of an export list checks that every component of the su‐
pertype is present in the subtype. The components of the subtype are calculated
by this function:
comps(EXPORTſ(Ds)) = Ds
comps(T1 ∧ T2 ) = comps(T1 ), comps(T2 )
m
comps((M1 : T1 ) × · · · × (Mn : Tn ) −→ T ) = [ ].
The result of applying comps to an intersection type might not be a well‐formed ex‐
port list. That’s because intersected module types may have repeated components
or may even be inconsistent. It turns out, however, that an inconsistent module
type can be accepted by the type system without causing a problem. For example,
T <: T1′ T <: T2′ comps(T) <: Ds ′
T <: T ′
T <: T1′ ∧ T2′ T <: EXPORTſ(Ds ′ )
9 Ds <: Ds ′
Ds <: [ ]
Molecule, abstract Ds = Ds pre , D, Ds post D <: D′′ Ds <: Ds ′′
data types, and
Ds <: (D′′ , Ds ′′ )
modules
566 D <: D′
t :: b∗cπ <: t :: b∗cπ′ t = τ <: t :: b∗cπ′ t = τ <: t = τ
T <: T ′
t :: b∗cπ <: t = π x : τ <: x : τ M : T <: M : T ′
a module type might be inconsistent because it claims two different identities for
a type t:
566. htranscript 530bi+≡ ◁ 561 567 ▷
‑> (module‑type INCONSISTENT
(allof (exports [type t Bool.t]) (exports [type t Int.t])))
module type INCONSISTENT =
(allof (exports [type t Bool.t]) (exports [type t Int.t]))
Because this module type has no inhabitants, it’s harmless. In particular, be‐
cause module type INCONSISTENT is uninhabited, proving a judgment of the form
INCONSISTENT <: T ′ causes no issues. Because INCONSISTENT has no inhabitants,
every inhabitant of INCONSISTENT is also an inhabitant of T ′ , so the judgment is
sound. And if you try to seal a module with type INCONSISTENT, you’ll find that you
can’t—although you might be frustrated by the error messages.
Once function comps produces components, they are checked using judgment
Ds <: Ds ′ . If Ds ′ is empty, it is supplied by any sequence of components. If Ds ′ is
nonempty, then it has the form (D ′′ , Ds ′′ ), which is supplied if both D ′′ and Ds ′′
are supplied. Components may be supplied in any order.
A single supplied component is checked using judgment D <: D ′ . An abstract
type may be supplied by another abstract type or by any manifest type—to have an
abstract type supplied by a manifest type is the essence of sealing. A manifest type
may be supplied either by an identical manifest type or by an abstract type that is
demonstrably equal to it. A value may be supplied only by a value of the same type,
and a module of type T ′ may be supplied by a module of the same name whose
type T is a subtype of T ′ .
A module can be sealed with any module type whose promises are redeemed by
the module. Gloriously, every well‐typed module has a principal module type that
is at least as good as any module type whose promises are redeemed by the mod‐
ule. For modules, “at least as good” means “a subtype of”—and, if you study the
subtype relation, you’ll see that it also means “provides at least as much informa‐
tion as.” A principal module type (henceforth, “principal type”) provides as much
information as possible; it hides nothing.
msubsn(bEXPORTſ(Ds)cπ ) = msubsn(bDscπ )
msubsn(bT1 ∧ T2 cπ ) = msubsn(bT1 cπ ) ◦ msubsn(bT2 cπ )
msubsn(b cπ ) = θI
msubsn(bt = τ , Dscπ ) = msubsn(bDscπ ) ◦ (π.t 7→ τ )
§9.8
msubsn(bt :: b∗cπ ′ , Dscπ ) = msubsn(bDscπ )
Molecule’s type
msubsn(bx : τ, Dscπ ) = msubsn(bDscπ ) system: Enforcing
msubsn(bM : bT cπ ′ , Dscπ ) = msubsn(bDscπ ) ◦ msubsn(bT cπ ′ ) abstraction
θI = the identity substitution 567
A module’s principal type is also intersection of all the types that could be as‐
cribed to it. In Molecule, a module’s principal type is unique up to reordering of
components. The existence of unique principal types is the result of careful design,
which I have borrowed from the ML family of languages.
Principal module types are used primarily for sealing. The module being sealed
is the implementation, and its type is the implementation type. The module type doing
the sealing is the interface type or just the interface. Before the two can be compared,
the interface type has to be realized. To see that the interface type can’t always be
compared directly, consider the example below: module R has principal module
type (exports [type t Int.t] [x : Int.t]), and it is sealed with an interface in
which not only is t abstract, but x is given the abstract type R.t, not type Int.t.
567. htranscript 530bi+≡ ◁ 566 570 ▷
‑> (module [R : (exports [abstype t] [x : t])]
(type t Int.t)
(val x 1983))
module R : (exports [abstype t] [x : R.t])
As illustrated by the Char example at the start of this section, types R.t and Int.t
are different, but we want the example to be accepted anyway. After all, the inter‐
face places no constraints on type t, and the t and x components demanded by the
interface are provided by the implementation.
The type component t is easy to accept; the interface demands an abstract‐
type component t :: b∗cR , and the module’s principal type has component t =
Int.t. According to Figure 9.15, any manifest type t is a subtype of abstract type t.
All good.
What about x? The interface demands value component x : R.t, and the im‐
plementation’s principal type has component x : Int.t. Just like types Char.t and
Int.t, types R.t and Int.t are different, and according to Figure 9.15, one value
component is a subtype of another only if their types are the same. But inside mod‐
ule R, they are the same—the question is, how does the type system know? Molecule
uses a technique that is borrowed from Standard ML, called realization.
An interface is realized by replacing each abstract type with its representation,
which comes from the implementation. Realization also transforms each abstract‐
type component into a manifest‐type component. The type definitions in the im‐
plementation are converted to a substitution by function msubsn (Figure 9.16),
which is applied to the principal type of the implementation. In the example of
module R above, msubsn returns the substitution that replaces type R.t with type
Int.t.
θ(EXPORTſ(D1 , . . . , Dn ) = EXPORTſ(θ(D1 ), . . . , θ(Dn ))
θ(T1 ∧ T2 ) = θ(T1 ) ∧ θ(T2 )
9
m m
θ((M1 : T1 ) × · · · × (Mn : Tn ) −→ T ) = (M1 : θ(T1 )) × · · · × (Mn : θ(Tn )) −→ θ(T )
θ(t = τ ) = t = θ(τ )
Molecule, abstract θ(t :: b∗cπ ) = t :: b∗cπ , when π ∈
/ dom θ
data types, and θ(t :: b∗cπ ) = t = θ(π), when π ∈ dom θ (⋆)
modules
θ(x : τ ) = x : θ(τ )
568 θ(M : T ) = M : θ(T )
d The definition
C The context in which it appears
E The environment in which it is checked
Ds The bindings or components that it contributes
The context is used to generate an absolute access path for every defined mod‐
ule and every new type defined by data. For example, in module Intlist below,
the module definition is checked in the empty context, but the data definition is
(DEFVAL)
E ` bdcC : Ds E`e:τ
E ` bVAL(x, e)cC : [x : τ ]
(DEFVALREC) (DEFTYPE)
E ` τ ⇝ τ′ E, x : τ ′ ` e : τ ′ E ` τ ⇝ τ′ §9.8
Molecule’s type
E ` bVAL‐REC(x : τ, e)cC : [x : τ ′ ] E ` bTYPE(t, τ )cC : [t = τ ′ ]
system: Enforcing
abstraction
(a) Core‐layer definition forms
569
(DEFMODTYPE)
E ` bdcC : Ds E ` bT c• ⇝ T ′
E ` bMODULE‐TYPE(T, T )c[ ] : [T = T ′ ]
(DEFSEALEDMODULE)
E ` bdscC[M ] : Tsub
E ` bT cC[M ] ⇝ Tsuper θ = msubsn(Tsub ) Tsub <: θTsuper
E ` bMODULE([M : T ], ds)cC : [M : Tsuper ]
(DEFCOPYMODULE)
E`π:T
E ` bMODULE(M, π)cC : [M : T ]
(DEFREſEALMODULE)
E ` π : Tsub
E ` bT cC[M ] ⇝ Tsuper θ = msubsn(Tsub ) Tsub <: θTsuper
E ` bMODULE([M : T ], π)cC : [M : Tsuper ]
(DEFGENERıCMODULE)
m
Tg = [M1 : T1 ] × · · · × [Mk : Tk ] −→ T
E ` bTg c[ ] ⇝ Tg′
Tg = [M1 : T1′ ] × · · · × [Mk : Tk′ ] −→ T ′
′ m
πb = (@m C[M ] M1 · · · Mk )
E, M1 : T1′ , . . . , Mk : Tk′ ` bdscπb : Tb
θ = msubsn(Tb ) Tb <: θT ′
E ` bGENERıC‐MODULE([M : Tg ], ds)cC : [M : Tg′ ]
E ` bdscπ : T E ` bdscπ : Ds
E ` bdscπ : EXPORTſ(Ds)
(NONEMPTYDEFſ)
E ` bdscπ : Ds E ` bdcπ.[ ] : Ds ′
E, Ds ′ ` bdscπ : Ds ′′
dom(C(Ds)) ∩ dom(Ds ′′ ) = ∅
E ` b cπ : [ ] E ` bd, dscπ : C(Ds), Ds ′′
checked in the context Intlist.[ ]. That context makes the type’s absolute access
path Intlist.t.
570. htranscript 530bi+≡ ◁ 567 593 ▷
‑> (module [Intlist : (exports [abstype t] [Nil : t] [Cons : (int t ‑> t)])]
(data t
[Nil : t]
[Cons : (int t ‑> t)]))
module Intlist :
(exports
[abstype t]
[Nil : Intlist.t]
[Cons : (Int.t Intlist.t ‑> Intlist.t)])
Definitions are checked by the rules shown in Figure 9.18. Because there are
a lot of definition forms, there are a lot of rules; part (a) shows rules for just the
core‐layer forms. Rule DEFVAL resembles the VAL rule for Typed µScheme; expres‐
sion e is typechecked to have type τ , and the result binding list Ds is the singleton
list [x : τ ]. In other words, x : τ is produced as a binding (and a component).
Unlike VAL forms, VAL‐REC and TYPE forms include types in the syntax, and
those types have to be checked. A type is checked by a judgment of the form
E ` τ ⇝ τ ′ , where type τ is the type written in the syntax and τ ′ is the internal
representation used in the type checker. The judgment, pronounced “τ elaborates
to τ ′ ,” produces τ ′ by expanding all the type abbreviations found in τ , and it also
checks that τ is well formed. Elaboration, which is presented in the next section,
subsumes the kind checking used in Typed µScheme ( judgment ∆ ` τ :: ∗).
With the understanding that types have to be elaborated, rules DEFVALREC and
DEFTYPE are otherwise similar to DEFVAL: each rule produces one binding.
In part (b), on module‐layer definition forms, the DEFMODTYPE rule resembles
the DEFTYPE rule. The elaboration of a module type is more involved than the elab‐
oration of a type, and it too is discussed in the next section. And there’s one more
subtlety: a MODULE‐TYPE definition typechecks only when evaluated in the empty
context [ ]. That requirement ensures that module types may be defined only at top
level.
The remaining rules typecheck module definitions. The main rule is DEF‐
SEALEDMODULE: a module M is sealed with interface T and defined by a sequence
of definitions ds . Above the line, the first judgment E ` bdscC[M ] : Tsub says that
the principal type of the implementation is Tsub . Next, the interface type is elabo‐
rated to produce internal representation Tsuper . Then, as described in the previous
section, the interface type is realized by substituting for every manifest type in Tsub .
Provided the principal type is a subtype of the realized interface type, the module
definition is well typed, and a binding is produced that associates the module with
its (unrealized) interface type, not with its principal type. This step, in this rule, is
where a manifest type inside M is converted to an abstract type outside—it’s where
data abstraction happens.
Rules DEFCOPYMODULE and DEFREſEALMODULE are similar, except the mod‐
ule’s body is obtained not from a sequence of definitions but from another module
§9.8
named at path π . Judgment E ` π : Tsub says that the module at π has princi‐
Molecule’s type
pal type Tsub , and that type is obtained in two steps: first the path π is looked up
system: Enforcing
in the environment, and then the type of the module at that path is strengthened.
abstraction
Strengthening converts every abstract type into a type that is manifestly equal to
itself (Figure 9.19, on the facing page). The strengthening transformation is used 571
in the single rule for judgment E ` π : T ; both are shown in Figure 9.19.
Rule DEFGENERıCMODULE looks complicated, but it’s more of the same. A ge‐
neric module has to have a module‐arrow type, and that type is elaborated to pro‐
duce the internal representations of the argument types T1′ , . . . , Tk′ and the result
type T ′ . The body of the module ds is checked at path πb , which is the path formed
by instantiating the module at its formal parameters. (When the module is instanti‐
ated, the formal parameters are replaced by the absolute access paths of the actual
parameters.) And the body is checked in an extended environment, which has ac‐
cess to the formal parameters. Finally, in the usual way, the body’s principal type Tb
is checked to make sure it delivers everything demanded by interface type T ′ . If all
is well, the generic module typechecks with the module type claimed for it, and a
binding is produced with that type. Because a generic module appears only at top
level, this binding won’t ever be converted to a component.
Figure 9.18c shows how to check a sequence of definitions in the body of a mod‐
ule. The principal type of a sequence of a definitions is always an EXPORTſ type,
and definitions are checked one at a time. The key rule is rule NONEMPTYDEFſ,
which checks definitions bd, dscπ at path π . Definition d is checked first, and the
resulting bindings Ds ′ are added to the environment that is used to check the re‐
maining definitions ds . But not everything in Ds ′ necessarily contributes a com‐
ponent. The sequence of bindings Ds ′ is converted into a sequence of components
by function C , which is defined as follows:
C([ ]) = [ ]
C(t = τ, Ds) = t = τ, C(Ds)
C(x : τ, Ds) = x : τ, C(Ds)
C(M : bT cπ , Ds) = M : T , C(Ds)
C(x ∈ [τ1 , . . . , τn ], Ds) = C(Ds).
The rule has a side condition: a module may not have duplicate components.
This condition is enforced by the premise dom(C(Ds ′ )) ∩ dom(Ds ′′ ) = ∅. In my
implementation, this condition is relaxed: a value component—and only a value
component—may be redefined. Redefining a function can be useful when debug‐
ging.
As in Typed µScheme and µML, type syntax written in a program must be vali‐
dated. Because every type in Molecule has kind ∗, validation is less involved than
in Typed µScheme or µML: it suffices if every path is well formed and refers to a
type. And if such a path begins with an instance of a generic module, the module
must be instantiated correctly. Molecule’s type checker not only validates syntax
but also translates it into internal form: it replaces each type abbreviation with its
E ` τ ⇝ τ′ E ` τi ⇝ τi′ , 1 ≤ i ≤ n E ` τ ⇝ τ′
E ` τ1 × · · · × τn → τ ⇝ τ1′ × · · · × τn′ → τ ′
9 E3t=τ E 3 π.t = τ
E`t⇝τ E ` π.t ⇝ τ
Molecule, abstract
data types, and (a) Elaboration of types
modules
E ` bT cπ ⇝ T ′ (ELABINTERſECTıON)
572 (ELABNAMEDMT) E ` bT1 cπ ⇝ T1′
E3T =T E ` bT2 cπ ⇝ T2′
T ′ = (• 7→ π)T θ = msubsn(T1′ ∧ T2′ )
E ` bT cπ ⇝ T ′ E ` bT1 ∧ T2 cπ ⇝ θT1′ ∧ θT2′
(ELABEXPORTſ) (ELABGENERıC)
E ` bDscπ ⇝ Ds ′ bEc[ ] ` bT cπ ⇝ T ′
E ` bEXPORTſ(Ds)cπ ⇝ EXPORTſ(Ds ′ ) E ` bT cπ ⇝ T ′
E ` bDscπ ⇝ Ds ′ E ` bDcπ ⇝ D′
E, B(bD′ cπ ) ` bDscπ ⇝ Ds ′
dom(D) ∩ dom(Ds) = ∅
E ` b cπ ⇝ [ ] E ` bD, Dscπ ⇝ D′ , Ds ′
E ` bDcπ ⇝ D′ E ` τ ⇝ τ′
E ` bt :: ∗cπ ⇝ t :: b∗cπ.t E ` bt = τ cπ ⇝ t = τ ′
(ELABSUBMODULE)
E ` τ ⇝ τ′ E ` bT cπ.M ⇝ T ′
E ` bx : τ cπ ⇝ x : τ ′ E ` bM : T cπ ⇝ M : T ′
bEcπs ` bT cπ ⇝ T ′
(ELABGENERıCARG)
n>0 E ` bT1 cπ ⇝ T1′
m
bE, π.M1 : T1′ cπs,M1 ` b(M2 : T2 ) × · · · × (Mn : Tn ) −→ T cπ ⇝
m
(M2 : T2′ ) × · · · × (Mn : Tn′ ) −→ T ′
m
bEcπs ` b(M1 : T1 ) × · · · × (Mn : Tn ) −→ T cπ ⇝
m
(M1 : T1′ ) × · · · × (Mn : Tn′ ) −→ T ′
(ELABGENERıCREſULT)
E ` bT c(@m π πs) ⇝ T ′
bEcπs ` b −→ T cπ ⇝ −→ T ′
m m
• When the structural elaboration works its way down to an EXPORTſ type
(rule ELABEXPORTſ), the exported declarations are elaborated one by one in
sequence (Figure 9.20c). Once a declaration is elaborated, it is also added to
the environment, for which purpose it is converted to a binding. Each defini‐
tion is converted by function B , which treats every abstract type as an abbre‐
viation for its absolute access path (Figure 9.21). For example, when module
type 2DPOINT is elaborated (chunk 531a), the abstract type t is first added to
the environment as an abbreviation for its absolute access path •.t. Next,
when declaration for new is elaborated, it gets type (Int.t Int.t ‑> •.t).
(LOOĸUPVALUECOMPONENT)
E 3 π : bT cπ′ x : τ ∈ comps(T )
E 3 π.x : τ
(LOOĸUPMODULECOMPONENT)
E 3 π : bT cπ′ M : bT ′ cπ′′ ∈ comps(T )
E 3 π.M : bT ′ cπ′′
(LOOĸUPINſTANCE)
E 3 π : (M1 : T1′ ) × · · · × (Mn : Tn′ ) −→ T ′
m
E ` πi : T i , 1 ≤ i ≤ n
((M1 : T1′ ) × · · · × (Mn : Tn′ ) −→ T ′ )@(π1 : T1 · · · πn : Tn ) : T ′′
m
E 3 (@m π π1 · · · πn ) : bT ′′ c(@m π π1 · · · πn )
m
((M1 : T1′ ) × · · · × (Mn : Tn′ ) −→ T ′ )@(π1 : T1 · · · πn : Tn ) : T ′′
(INſTANTıATE)
θm = msubsn(T1 ) θr = (M1 7→ π1 ) T1 <: θm (θr T1′ )
((M2 : θr T2 ) × · · · × (Mn : θr Tn ) −→ θm (θr T ))@(π2 : T2 · · · πn : Tn ) : T ′′
′ ′ ′
m
( −→ T ′′ )@( ) : T ′′
m
(INſTANTıATEWıTHONEPARAMETER)
E 3 π : (M1 : T1′ ) −→ T ′
m
E ` π1 : T 1
θr = (M1 7→ π1 ) θm = msubsn(T1 )
T1 <: θm (θr T1′ ) T ′′ = θm (θr T ′ )
.
E 3 (@m π π1 ) : bT ′′ c(@m π π1 )
• The module being instantiated is π , and it has a module‐arrow type with one
formal parameter M1 of type T1′ . The actual parameter π1 has type T1 .
• The module type of the formal parameter, T1′ , can include references to
type components of that parameter. For example, in the generic mod‐
ule ArrayHeap defined in chunk 552a, the formal parameter is Elem, and
type T1′ declares a value <= of type (Elem.t Elem.t ‑> Bool.t). Module
ArrayHeap can be instantiated with actual parameter Int only because Int.t
and Elem.t are the same. The identity is demonstrated by “re‐rooting” mod‐
ule type T1′ using substitution θr , which maps M1 to π1 . In the example,
it maps Elem to Int.
• Once the module type of the formal parameter is re‐rooted, the actual param‐
eter is checked using subtyping, just as when sealing. And as when sealing,
θm substitutes for every manifest type in T1 . In the example, the module
type of the actual parameter, Int, doesn’t have any manifest types, so θm is
the identity substitution.
• Provided the subtyping test passes, the instantiated module has type T ′′ .
Type T ′′ is obtained by taking the result type T ′ from the module‐arrow type,
then substituting both for M1 and for any manifest types that occur in T1 .
The substitution says, for example, that in module (@m ArrayHeap Int), type
elem is an abbreviation for type Int.t.
If the types of generic modules were curried, so that a generic module al‐
ways took exactly one parameter, the special‐case rule would be enough. Multi‐
parameter modules require more bureaucracy. Actual parameters π1 , . . . , πn are
checked one at a time against formal parameters M1 , . . . , Mn . And the type com‐
ponents of actual parameter π1 can be referred to not only in the result type T ′ , but
4
Here D stands for a binding, which resembles a declaration or component.
E ` bdcC : Ds E ` π.x : τ
E does not overload x
E ` bOVERLOAD(π.x)cC : x ∈ [τ ]
9 E ` π.x : τ
E 3 x ∈ [τ1 , . . . , τk ]
Molecule, abstract
data types, and E ` bOVERLOAD(π.x)cC : x ∈ [τ, τ1 , . . . , τk ]
modules
(a) Typing rules for overload definition
576
E ` e ⇝ e′ : τ E ` ei ⇝ e′i : τi , 1 ≤ i ≤ n
E 3 x ∈ [τ1′ , . . . , τk′ ]
′
τj = τ1 × · · · × τn → τ
∄k : k < j ∧ τk′ = τ1 × · · · × τ ′′ → τ ′′′
E ` APPLY(x, e1 , . . . , en ) ⇝ APPLY(x, e′1 , . . . , e′n )j : τ
E ` ei ⇝ e′i : τi , 1 ≤ i ≤ n
E 3 x : τ1 × · · · × τn → τ
E ` APPLY(e, e1 , . . . , en ) ⇝ APPLY(e, e′1 , . . . , e′n ) : τ
also in the types of later formal parameters M2 to Mn . So after each actual param‐
eter, substitutions θr and θm have to be applied not only to the result module type,
but also to the remaining formal parameters. To apply those substitutions correctly
requires an auxiliary judgment, which takes up the last two rules in Figure 9.22.
9.8.8 Overloading
E ` e ⇝ e′ : τ
.
E`e:τ
But when a definition of the form VAL(x, e) is typechecked in the interpreter, every
expression is elaborated.
Molecule’s type system works can usefully be compared with other type systems
along three dimensions: polymorphism, generativity, and substitution.
• Both type systems have a function type (in the interpreters, FUNTY).
• Both type systems have abstract “type formers” of higher kinds, like “list” or
“array.” Such an abstraction is not a type by itself, but once supplied with ac‐
tual parameters, it can produce a type. In Typed µScheme, the abstraction
is a type constructor of kind ∗ ⇒ ∗, or of any other kind with an arrow in it.
In Molecule, the abstraction is a generic module—that is, a module whose
module type has an arrow in it. In Typed µScheme, a type constructor pro‐
duces a type when it is applied. In Molecule, a generic module produces
a type when it is instantiated and a type component is selected from the in‐
stance. These constructions are analogous; for example, the Typed µScheme
type (array bool) is analogous to the Molecule type (@m Array Bool).t.
• Both type systems can create a polymorphic value by abstracting over un‐
known types, and both enable code to refer to an unknown type by name.
In Typed µScheme, a polymorphic value is introduced by a type‑lambda, and
the unknown type is named by a type variable written in the type‑lambda.
In Molecule, a polymorphic value is not introduced directly; instead, Mol‐
ecule code introduces a generic module, then selects a value component.
The unknown type is a type component of an unknown module, which is
named by a formal module parameter in the generic module.
Generativity
Molecule’s module definitions are generative, in the sense explained in Section 8.5
(page 483): every module definition introduces new types, even if the module is
defined with the same name as a previous module. Generativity is not expressed in
Molecule’s type system as it is presented in this section—to track it would require
so much bookkeeping that the main ideas would be hard to follow. For that reason,
the type system as presented is sound only under the assumption that every module
has a distinct absolute access path. Under the covers, my implementation makes
it so:
• A path may begin with a module identifier Y , which cannot be written in the
syntax.
The uniqueness of the module identifiers ensures the uniqueness of every abso‐
lute access path, which makes module definitions generative and helps guarantee
soundness.
Molecule enforces type abstraction by making sure that outside a module, an ab‐
stract type looks different from its definition—and types that look different always
are different. When types look different but actually are the same, like int and
Int.t. Molecule makes them the same via elaboration and substitution: int is re‐
placed by Int.t; references to manifest types are replaced by their definitions; and
when a generic module is instantiated, the formal parameters are replaced by the
actual parameters. But substitution is nobody’s favorite mechanism: it can be hard
to get right, and when substitution is used in an inference rule, what it’s doing is
not always obvious. Substitution is a good choice for this chapter because it keeps
the type system consistent with the type systems presented in Chapters 6 and 7,
but a nice alternative is to define type equality and subtyping in a way that is not
consistent with Chapters 6 and 7: they can depend on context.
This alternative, designed by Leroy (2000), extends the core‐layer type system
with a contextual type‐equality judgment E ` τ ≈ τ ′ , and it replaces the module‐
layer subtyping judgment with the judgment E ` T <: T ′ . These judgments have
access to all the type equalities in environment E ; for example, Molecule’s initial
environment E0 supports the judgment E0 ` int ≈ Int.t. Making these judg‐
ments contextual enables Leroy’s system to check ascriptions without substituting;
for example, instead of extracting a substitution from the manifest‐type declara‐
tions of a module, his system puts those declarations into E . The system is elegant,
but its test for type equality is so different from the tests in Chapters 6 and 7 that it
is a poor fit here.
As you might guess from the size of the type system, Molecule’s interpreter is a
beast; it’s about the size of the interpreters for Typed µScheme and µML combined.
But the type checker uses the same techniques that are used in Typed µScheme, and
the evaluator uses the same techniques that are used in µML, so the code doesn’t
have much to teach you. The interpreter is therefore relegated to the Supplement.
Should you choose to study it, its most salient aspects are as follows:
• Many syntactic forms have two representations: one for the form as it ap‐
pears in the source code and one for the form as it appears after elaboration.
They are, for example, pathex and path, tyex and ty, modtyex and modty,
and so on.
• Expressions have only one representation; in that representation, the APPLY
node includes a mutable cell. This cell holds the decoration, if any, that iden‐
tifies the resolution of an overloaded name. Elaboration is implemented not
by translating from one representation to another but by updating that cell.
9 • The implementation includes a type not shown in Section 9.8: type ANYTYPE
is the type assigned to an error expression. Type ANYTYPE is uninhabited, so
Molecule, abstract it is compatible with any type.
data types, and
modules • In the implementation, unlike the type theory, declarations, components,
and bindings have different representations: decl, component, and binding.
580 In the theory, blurring the distinction simplifies the notation and makes the
ideas easier to follow. In the code, enforcing the distinction helps me get the
invariants right.
Different languages provide abstract data types and modules in very different ways.
To add to the potential confusion, many languages, including Java, C++, Modula‐3,
and Ada 95, combine abstract data types with objects. In this section, I stick to
abstract data types: I enumerate some major design choices, I describe what I con‐
sider the best choices, and I describe a key mechanism that’s not included in Mol‐
ecule: exceptions. I also say a few words about overloading.
Designs for abstract types and modules vary along many dimensions. The impor‐
tant variations can be identified by a litany of questions. When you encounter a
new language, the questions will help you understand what you’re dealing with.
• Can modules be generic? In Modula‐3, Ada, and the ML family, yes. In Modula‐2,
Oberon, and Haskell, no. (In C++, Java, and CLU, you will find some form of
generics, but not exactly modules.)
• Do generic modules support modular typechecking? That is, can you typecheck
a generic module and its arguments separately, then combine them? (If not,
only instances can be typechecked.) In the ML family, Java, and Molecule,
yes. In Modula‐3 and Ada, no. And in C++, templates don’t support modular
typechecking.
• Can a module export manifest types as well as abstract types? In CLU, no. In all
subsequent languages, yes. (Modula‐3 has an especially nifty mechanism,
the partial revelation, which can expose selected information about a type.)
• Does every module have a name? In CLU, Ada, the Modula family, Oberon,
Haskell, and Molecule, yes. In the ML family, no—modules can be anony‐
mous.
• Can modules nest? In CLU, Ada, the Modula family, and Oberon, no. In the
ML family and Molecule, yes. And in Haskell, a module cannot nest inside §9.10
another module, but the name space of modules is hierarchical, which gives Abstract data
the appearance of nesting—and some of the same benefits. (When a system types, modules,
grows large, nested modules help programmers avoid naming conflicts.) and overloading as
they really are
• What are things called? An interface has been called a “signature” (Stan‐
581
dard ML and OCaml), a “module type” (Molecule and OCaml), a “defini‐
tion module” (Modula family), a “package specification” (Ada), an “inter‐
face” (Java), and who knows what else. An implementation has been called
a “structure” (Standard ML and OCaml), a “module” (Molecule and Haskell),
an “implementation module” (Modula family), a “package body,” (Ada), and
who knows what else. A generic module has been called “generic” (Mole‐
cule, Modula–3, Ada) and a “functor” (Standard ML and OCaml).
In any given language, many of these questions will be answered differently than
they are in Molecule, but experience with Molecule will still give you a feel for con‐
sequences. However, that experience won’t teach you just how good generic mod‐
ules are when they are paired with a polymorphic core layer, as in Standard ML
and OCaml.
The design choices that matter most are those that determine how you express or
discover what operations a module exports. Can you work with a separately com‐
piled interface, or do you have only the implementation? Ideally, you can write
client code based only on an interface, and you don’t need to look at an implemen‐
tation. But there are trade‐offs:
• When names, types, and other information are explicit in the interface,
all the information needed to write client code is gathered in one place,
and the cognitive load involved in understanding the interface is indepen‐
dent of the size of the implementation. I claim that when interfaces are de‐
fined in this style, you can work with larger systems than you could other‐
wise. Other designers agree; for example, the designers of Modula‐3 write,
“To keep large programs well structured, you either need super‐human will
power, or proper language support for interfaces” (Nelson 1991, §1.4.1).
All three alternatives lead to annoying code, especially in a language without first‐
class, nested functions.5
Languages like C, C++, Java, and Modula‐3 offer another alternative: because
abstractions are represented by pointers, an operation with no other recourse can
often return a null pointer. In Molecule, CLU, Haskell, and ML, by contrast, there
is no such thing as a null pointer. We should be grateful; Tony Hoare, the inventor
of the null pointer, called it a “billion‐dollar mistake” (Hoare 2009). Exceptions are
better.
Exceptions appear in many major languages, including Ada, C++, Haskell, Java,
ML, and Modula‐3. Exceptions are typically supported by two syntactic forms, var‐
iously called raise and handle, signal and except, throw and try‑catch (Sec‐
tion 3.2.2, page 207), or similar names. The handle/except/try‑catch form defines
a handler, inside of which code is evaluated. When a raise is evaluated, it termi‐
nates the evaluation of an expression, statement, or code block, and it transfers
control to the nearest enclosing handler. If the raise is not enclosed in a handler
in its own function, then it terminates the evaluation of its function and looks for
a handler in the caller. If the call site isn’t enclosed in a suitable handler, then
the caller is terminated as well, and this computation—unwinding the call stack—
continues until it reaches a call site that is enclosed in a suitable handler.
As an example, interpreters from Chapter 5 onward raise the RuntimeError ex‐
ception in many places, and in each case, the call stack is unwound until control
5
If you have true higher‐order functions, you can design a lot of code around a polymorphic error
type like the one in Appendix H, and some of the suffering caused by working with sum types can be
mitigated by higher‐order functions like >>= and >>=+, which are defined there.
reaches the handler in the read‐eval‐print loop. At that point, the read‐eval‐print
loop prints an error message, and evaluation continues with the next definition.
Exceptions have been used in different ways at different times. Exceptions
as we know them originated with CLU, but CLU imposed two restrictions that we
9 would find severe today: in CLU, the set of exceptions a function can raise must
be enumerated in that function’s type, and every exception that a function might
raise must be handled by its caller. These restrictions enabled a remarkably ef‐
Molecule, abstract
ficient implementation: Adding a handler to a statement entailed no cost at run
data types, and
time, and raising an exception, at least on machines of the day, cost barely more
modules
than using return. In particular, an exception could be raised without having to
584 allocate anything on the heap.
CLU’s exceptions were intended for more than just errors; they were recom‐
mended to be used to ensure that every operation does something well‐defined on
every argument in its domain. For example, exceptions were used to indicate such
ordinary, non‐error situations as hitting end of file on input, not finding a name in
an environment, or trying to choose a value from an empty set.
Things have changed. While in some languages the exceptions a function can
raise are still part of its type, we no longer require every caller to handle every pos‐
sible exception. And raising an exception has gotten more expensive—for example,
the designers of Modula‐3 recommend to spend a thousand instructions per excep‐
tion raised if that expenditure will save one instruction per procedure call (Nelson
1991). Moreover, attitudes have changed. Many respected programmers believe
that exceptions should be reserved for truly exceptional events. I personally find
that routine use of exceptions leads to cleaner interfaces, but I know that the cost
models aren’t what they were in CLU. If I intend to use exceptions routinely, I pay
close attention to costs.
Overloading as it really is
From the time there has been floating‐point arithmetic, language designers have
wanted operators like + to mean either integer or floating‐point arithmetic, depend‐
ing on the types of arguments. Early designs were ad hoc and often unsatisfying;
for examples, look no further than the two implementation languages of this book:
C and Standard ML.
In the 1970s, CLU took a nice step forward: CLU uses overloading to define a
whole bunch of syntactic forms—including not only infix operators but also dot no‐
tation and assignment statements—as notations for function application. CLU’s no‐
tations work equally well with both built‐in types and user‐defined types; CLU over‐
loads a fixed set of syntactic forms, each of which takes a fixed number of argu‐
ments, by dispatching each one to a well‐known qualified name, as determined by
the type of the first argument. For example, an expression of the form e1 + e2 is
rewritten to function call M.add(e1 , e2 ), where M is the module that defines the
type of e1 . CLU’s mechanism inspired the algorithm I use in Molecule.
In 1981, Ada extended overloading to include not just a fixed set of syntactic
forms (often called “operator overloading”) but also user‐defined functions (often
called “function overloading,” or in Ada jargon, “subprogram overloading”). Ada’s
overloading was notable for two innovations: first, user‐defined functions could
be overloaded not just based on the types of the arguments but also on the num
ber of arguments—something not necessary when overloading a syntactic form
like exp + exp, which always has exactly two subexpressions. Second, and more
startling, functions could be overloaded based on their result types, enabling the
context in which a call appears to influence what function is called. Ada’s ideas for
overloading were adapted for use in both C++ and Java, although neither C++ nor
Java supports overloading based on a result type.
In the 1990s, Haskell adopted a mechanism proposed by Wadler and Blott
(1989): functions are overloaded using type classes. A type class can be used to over‐
load any name; because a Haskell “operator” is just an ordinary function name
written in infix notation, type classes provide both operator overloading and func‐
tion overloading. Type classes defy simple description, but their most important
element is a relation that says “this type implements this operation using this func‐
§9.11
tion.” The relation is established using an instance declaration; for example, an in‐
Summary
stance declaration can be used to establish that machine integers implement + us‐
ing a primitive function, while complex numbers implement + using a function 585
that independently adds the real and imaginary parts. Crucially, an instance dec‐
laration takes the form of an inference rule; for example, Haskell includes a rule
that says, in effect, that if τ is a type that implements print, then “list of τ ” is also
a type that implements print. The rule also says how to construct the print func‐
tion for a list of τ . Type classes are worth mastering; the ability to tell the compiler
to construct new functions by combining inference rules has had implications far
beyond the original goals of overloading. For a nice example, see Claessen and
Hughes (2000).
9.11 SUMMARY
Abstract data types as we know them are descended from CLU, whose designers’
goal was “to provide programmers with a tool that would enhance their effective‐
ness in constructing programs of high quality—programs that are reliable and rea‐
sonably easy to understand, modify, and maintain” (Liskov et al. 1977). Abstract
data types and modules separate concerns and hide information, limiting the scope
of modifications.
By focusing attention on abstractions and their operations, not representa‐
tions, abstract data types and modules make programs relatively easy to under‐
stand. Ideally, focus can be directed by an explicit interface, like a module type,
which can be written, read, and typechecked independently of any implementation
that it describes.
Any form of abstract data type has a public name but a private representa‐
tion. Access to the representation is limited to operations defined inside some
sort of syntactic “capsule,” like a module. But within the capsule, each operation
can inspect the representation of every argument of the abstract type. And be‐
cause access is granted by type, defining an operation that takes multiple abstract
arguments—like the sum of two arbitrary‐precision integers or the merge of two
leftist heaps—is easy.
The major limitation of abstract data types is that every access is controlled by
a static type, and every representation is fixed by a static type. Strict access control
means that different implementations of similar abstractions do not interoperate;
for example, a machine integer cannot be added to an arbitrary‐precision integer.
Interoperation is possible only if a programmer inserts explicit coercions. And a
new representation can be added to an existing abstraction only by modifying the
source code, which may change the cost model or even the interface.
ABſTRACT DATA TYPE A form of DATA ABſTRACTıON that operates using a static
type system. An abstract data type has a public name and a private REPRE‐
ſENTATıON. The representation is visible only inside a syntactic “capsule,”
like a MODULE. Operations defined within the capsule have access to the
representation of any value of the abstract type.
API All the information needed to write CLıENT code that uses an ABſTRACTıON.
Not just a list of exported operations and their types, an API also says how
each operation behaves and when it is permissible to use it. Also called a
COMPLETE API.
CLıENT Code that uses the operations of an ABſTRACTıON and does not have access
to the abstraction’s REPREſENTATıON.
DATA ABſTRACTıON The practice of characterizing data by its operations and their
specifications, not by its REPREſENTATıON.
INTERFACE The primary unit of design: Separately compiled syntax that specifies
whatever properties of an ABſTRACTıON are needed to get CLıENT code to
typecheck. In Molecule, an interface is a MODULE TYPE, which gives names
and types of the exported operations, plus the types of any nested modules.
“Interface” is also used to mean a complete API.
SUBTYPıNG A relation T <: T ′ which says that any MODULE of type T also has
type T ′ —and therefore can be used wherever a module of type T ′ is ex‐
pected. A programming language may also offer a subtype relation on the
types of expressions.
Data abstraction is a form of information hiding, for which best practices were de‐
veloped in the 1970s. Parnas (1972) argues that an effective module hides a de‐
sign decision that is likely to change. Such decisions include input formats, out‐
put formats, and algorithms, as well as the decisions emphasized in this chapter:
the representations of abstractions. Parnas is sometimes paraphrased as saying
“every module hides a secret.” Wirth (1971) describes top‐down development of
programs by “stepwise refinement” of high‐level specifications into working code.
He emphasizes that decisions about representation should be delayed as long as
possible (and thereby hidden from one another). Dahl and Hoare (1972) say that
“good decomposition means that each component may be programmed indepen‐
dently and revised with no, or reasonably few, implications for the rest of the sys‐
tem.” They argue for a language mechanism based on the objects and classes of the
Simula 67 programming language; the realization of those ideas in Smalltalk is the
subject of Chapter 10 of this book.
The implementation of an abstract data type can be proved correct using a
method developed by Hoare (1972). Hoare’s paper introduces the ideas of abstrac‐
tion function and representation invariant, and it argues that the method is essen‐
tial for succinct specifications. If you like math with your code, read it.
9 Languages that support abstract data types owe a great deal to CLU. CLU’s ini‐
tial ideas are described by Liskov and Zilles (1974). At that point compile‐time type
checking was just a glimmer on the horizon; the paper emphasizes a deep philo‐
Molecule, abstract
sophical difference between CLU and its predecessor Simula 67—Simula 67 was de‐
data types, and
signed to expose details of representation, and CLU was designed to hide them.
modules
A fully developed CLU, complete with compile‐time type checking, is described by
588 Liskov et al. (1977). The context in which CLU was developed—including the state
of programming languages before CLU, the connections between CLU and pro‐
gramming methodology, the major principles underlying CLU’s design, and CLU’s
history—is the subject of a later retrospective (Liskov 1996). And if you are inter‐
ested in an innovative aspect of the implementation, CLU’s exception mechanism
is described in a more technical paper (Liskov and Snyder 1979).
Beyond CLU, data abstraction plays an important or even central role in Sim‐
ula 67 (Dahl and Hoare 1972; Birtwistle et al. 1973), Alphard (Wulf, London, and
Shaw 1976), Modula‐2 (Wirth 1982), and Ada. Data abstraction can also be achieved
using lambda in Scheme, as described by Abelson and Sussman (1985, Chapter 2).
Abstract data types work very well for data structures; some classic, well‐
engineered examples are presented by Hanson (1996). Hanson’s implementations
are written in C, so he’s working with stone knives and bearskins, but the results
are both informative and useful. For more depth in the use of abstract types, and
to delve into the relationship between software development and software speci‐
fication, consult the excellent book on programming by Liskov and Guttag (1986).
And for an example of pushing generic modules beyond reasonable bounds, I rec‐
ommend my own work on building a type‐safe, separately compiled, extensible
interpreter (Ramsey 2005).
Modules and modular type checking are beautifully explained by Leroy (1994),
who opens with two pages that explain what we ought to expect from modules and
interfaces, and why. I cannot recommend this work highly enough. The techni‐
cal part of the paper is equally strong, showing how to accommodate a mix of
abstract and exposed types in one interface; this problem was solved indepen‐
dently by Harper and Lillibridge (1994). Leroy (2000) follows up with a longer paper
that implements his ideas in a way that enables a compiler or interpreter to easily
add modules to an existing language. This work directly inspired Molecule’s type
system—Molecule’s module layer includes just one feature not taken from Leroy:
the intersection type, as proposed by Ramsey, Fisher, and Govereau (2005).
Modules are not normally recursive or mutually recursive, and yet much ink
and even more thought have been expended on making them so. This work has
yet to find its way into wide use, but my favorite proposal replaces “modules” with
“units”; a “unit” is a syntactic capsule that puts imports and exports on equal foot‐
ing. Units are linked into larger units by a separate linking language (Flatt and
Felleisen 1998).
Modules are often viewed as competing with classes. In a talk by Leroy (1999),
the two are compared from both theoretical and practical perspectives. Leroy ar‐
gues that modules are most effective in situations where the set of kinds of things
(data) is likely to remain stable, but the set of operations performed on things is
fluid. Classes are most effective in situations where the set of operations performed
on things is stable, but the set of kinds of things is fluid. In a more technical
comparison, Cook (2009) focuses on fundamental semantic differences between
Table 9.24: Synopsis of all the exercises, with most relevant sections
abstract data types and objects and on the relationship between them. Cook also
discusses how both mechanisms are used in Java, Haskell, and Smalltalk.
In Haskell, names are overloaded using the type classes proposed by Wadler
and Blott (1989). Depth in the underlying theory is provided by Jones (1993). Type
classes are related to modules and module types by Dreyer et al. (2007). And they
are very cleverly applied by Claessen and Hughes (2000).
Syntactic forms raise and handle are not actually very good at expressing the
ways we usually use exceptions. For a lovely alternative, see the proposal by Benton
and Kennedy (2001).
9.12 EXERCıſEſ
Exercises are arranged mostly by the skill they call on (Table 9.24). Some of my
favorites, from least difficult to most difficult, are as follows:
3. New operation: clockwise rotation. Extend the 2Dpoint module with a rotate‑
operation, which rotates a point 90 degrees clockwise.
(a) Change the 2DPOINT module type to include an abstract type quadrant
and four values of that type, with names UPPER‑LEFT, UPPER‑RIGHT, and
so on. (Because these names begin with uppercase letters, they must be
value constructors.) Replace operation quadrant with a new operation
get‑quadrant of type (t ‑> quadrant).
(b) Change the implementation of the 2Dpoint module to conform to the
new interface. To define type quadrant and its value constructors, use
a data definition.
9.12.4 Change of representation
Represent a quadrant in whatever way you like; a small integer will do, but
you might prefer a symbol or the constructed data of Exercise 5.
(a) Define an internal invariant function that expresses all the invariants
of your representation. At minimum, magnitudes x‑mag and y‑mag
must be nonnegative.
(b) The representation stands for that unique point that is located an ab‐
solute distance of x‑mag from the y axis, y‑mag from the x axis, and
that is located in the given quadrant. Make this idea precise by defin‐
ing an abstraction function that maps your representation to a pair of
Cartesian coordinates (x, y). The abstraction function is to be applied
only to representations that satisfy the invariant; if applied to any other
representation, it can fail, or it can even give wrong answers.
(c) Update the operations so they work correctly with your new represen‐
tation. When you finish, the new version should be observationally
equivalent to the original: it should be impossible for any program to
tell which version of 2Dpoint it is using. Take special care with points
whose x or y magnitudes are zero.
This representation need not satisfy any invariants; all pairs of real and imag‐
inary parts are meaningful.
Using this representation, define a module Complex that implements integer
complex numbers.
Assuming types key and value are defined, a binary search tree can be represented
as constructed data of type bst:
593. htranscript 530bi+≡ ◁ 570 598 ▷
‑> (data bst [EMPTY : bst] [NODE : (bst key value bst ‑> bst)])
bst :: *
EMPTY : bst
NODE : (bst key value bst ‑> bst)
9. Order invariant on a binary search tree. Algebraic data type bst defines a binary
tree in which each node carries two values, one of type key and one of type
value. But not every value of this type is a binary search tree: a search tree
must obey an order invariant.
(a) Using the inductive structure of binary trees, write a proof system for
the judgment “tree T satisfies the order invariant.” Take advantage of
nondeterminism.
(b) Using your proof system, define a recursive function order‑invariant?
that checks if a value of type bst tree satisfies the order invariant.
12. Algorithmic invariants of addition. Prove that when large natural numbers
X and Y are added, the following invariants hold:
13. Proof of correctness of short division. Using the definitions of X , Q, and r from
Table B.2 in Appendix B, prove that
Q · d + r = X.
(a) Define a module type ISET which describes immutable sets. It should
export types t and elem, and it should export values emptyset, member?,
add‑element, union, inter, and diff.
(b) Define a module type MSET which describes mutable sets. It should ex‐
port types t and elem, and it should export values emptyset, member?,
and add‑element, as well as functions that compute union, intersec‐
tion, and difference of two sets. Each two‐set function should mu‐
tate its first argument, and those functions should be called add‑set,
intersect, and remove‑set.
(b) Using algebraic laws, as in Section 9.6.3, relate the empty, find, and
bind operations. You might want to revisit the algebraic laws for sets
(page 548) or laws in general (Section 2.5, page 110).
16. Specification for removal from association lists. Add a remove operation to your
module type for association lists (Exercise 15). The operation could work in
either of two ways:
(a) Calling (remove k ps) could return an association list that has no bind‐
ing for k. In other words, (remove k ps) could remove all bindings
for k. Write algebraic laws for this version of remove.
(b) Calling (remove k ps) could remove only the most recent binding for k.
(This is how association lists and hash tables work in OCaml.) This ver‐
sion of remove cannot be specified using the abstraction of a set of key‐
value pairs as in Exercise 15a, but it is easy to specify using algebraic
laws. Write algebraic laws for this version of remove.
(b) A mutable association list can implement bind using fewer allocations
than an immutable one: Even in the worst case, a mutable association
list should be able to implement bind by allocating at most one new
record. Including such a version of bind, implement your abstraction.
18. Mutability via reference. Module (@m Ref M) exports a type t which is a mu‐
table reference cell containing one value of type M.t. Such a cell is cre‐
ated using function new, read using function !, and written using func‐
tion :=. A reference cell can be used to create a mutable abstraction from
an immutable one. Demonstrate this trick by defining a generic module
MutablePQ, which should take one argument of type IMMUTABLE‑PQ, and
which, when instantiated, should have type MUTABLE‑PQ. (This trick provides
a mutable abstraction without providing the cost benefit usually associated
with mutable abstractions.)
9.12.8 Design with module types
20. Total order via relational operators. Define a module type ORD, which speci‐
fies an abstraction whose values of type t are totally ordered, supporting the
classic four inequality operators. Your definition of ORD should refer to EQ.
Verify that primitive module Int has module type ORD.
21. Total order via comparison function. Define a module type COMPARE, which
specifies an abstraction whose values of type t can be compared using a
compare function, which returns a value of type Order.t.
23. Numbers. Define a module type NUM, which specifies an abstraction that sup‐
ports addition, subtraction, negation, multiplication, and division, plus a
conversion function of‑int, which takes an argument of type Int.t and re‐
turns a value of the abstraction. Verify that module Int has module type NUM.
24. Printability. Define a module type PRINT, which specifies an abstraction that
exports functions print and println. Verify that modules Int, Sym, and so
on have module type PRINT.
25. Collection. Define a module type COLLECTION, which should describe a mu‐
table collection of elements. It should export types t and elem, plus three
operations:
26. Types of module Int. Verify, using a single check‑module‑type test, that mod‐
ule Int has module types EQ, ORD, NUM, and PRINT.
27. Types of module Complex. Verify that the complex‐number abstraction from
Exercise 8 has module types EQ, NUM and PRINT. And use check‑type‑error
to verify that it does not have type ORD.
28. Generic complex numbers via NUM and PRINT. Using an argument module that
has module types NUM and PRINT, define a generic module MkComplex, which
should be a generic version of the complex‐number module described in Ex‐
ercise 8.
Classic data structures—of the kind that are agnostic to the data they structure—are
§9.12
perfect candidates for generic modules. While comparable to the core‐layer poly‐
Exercises
morphism used to implement sets in µScheme and to the explicit polymorphism of
Typed µScheme, generic modules make polymorphism easier to use in two ways: 597
First, a generic module can depend on an operation as easily as it can depend on
a type. Second, instantiating a generic module specializes a whole group of opera‐
tions and types at once. These benefits are demonstrated in the exercises below.
29. Immutable list. A module type for Scheme‐like lists might look like this:
597. hlist.mcl 597i≡
(module‑type LIST
(exports
[abstype elem]
[abstype t]
[empty : t]
[null? : (t ‑> bool)]
[cons : (elem t ‑> t)]
[car : (t ‑> elem)]
[cdr : (t ‑> t)]
[app : ((elem ‑> unit) t ‑> unit)]
))
Implement a generic module List that takes one parameter, a module Elem,
and returns a module that implements LIST and also has the following mod‐
ule type:
30. Mutable list. Design and implement two variations of mutable list:
(a) A simple variation exposes the representation: client code can mutate
any car or cdr. This variation corresponds to a “linked list” that is often
taught to beginning programmers.
(b) A more sophisticated variation can provide some abstraction and a rep‐
resentation invariant. Design and implement a mutable list abstraction
in which the representation points to the last element of a circularly
linked list. For the representation, you can use your solution to part (a).
Such a representation enables you to implement a variety of observers
and mutators in constant space and time:
• Find first element or last element.
• Remove first element or last element.
• Add element at head or tail.
• Append two lists, mutating both (the first is left with the results of
the append and the second is left empty).
This representation is used to implement the List class that is built in
to µSmalltalk (Chapter 10).
31. Immutable association list. Using the interface from Exercise 15, implement
an immutable association list Alist with exported values empty, find, and
bind. Make it generic, taking key and value modules as parameters.
9 32. Binary search tree. Exercise 15 defines the interface not just for an association
list, but for any immutable finite map. Implement this abstraction, just as in
the previous exercise, but as the representation, use the binary search tree
Molecule, abstract bst defined in chunk 593.
data types, and
modules 33. Leftist heap. Finish the implementation of the leftist heap whose merge oper‐
ation is given in Section 9.7.1 (page 555). That is, implement operations new,
598 empty?, insert, and delete‑min. Do not write any new recursive functions—
any operation that involves more than one node should call merge.
34. Hash table. A generic hash table might have this module type:
598. htranscript 530bi+≡ ◁ 593 599a ▷
‑> (module‑type GENERIC‑HASH
([Key : (exports [abstype t]
[hash : (t ‑> int)]
[= : (t t ‑> bool)])]
[Value : (exports [abstype t])]
‑‑m‑>
(exports
[abstype t]
To maintain these invariants as key‐value pairs are added, the hash table has
to grow. In particular, it has to grow whenever an insert operation makes
population equal to grow‑at. Growth is easy to get wrong; I have used a
production system in which the hash table grew too slowly, the association
lists got too long, and eventually operations that should have taken constant
time took linear time instead. For ideas about monitoring the internal state
of the hash table, see Exercise 35.
I recommend initializing a hash table with n = 17 buckets, then growing n
using the following sequence of prime numbers:
599a. htranscript 530bi+≡ ◁ 598
‑> (val prime‑sizes
'(17 23 31 41 59 79 103 137 179 233 307 401 523 683 907 1181
1543 2011 2617 3407 4441 5779 7517 9781 12721 16547 21517 §9.12
27983 36383 47303 61507 79967 103963 135173 175727 228451
Exercises
296987 386093 501931 652541 848321 1102823 1433681 1863787
2422939 3149821 4094791 5323229 6920201 8996303 11695231 599
15203803 19764947 25694447 33402793))
A prime size uses of all the bits in the hash value effectively, and each of these
primes is at least 1.3 times larger than the one the precedes it.
Using these guidelines, implement a generic hash table Hash that grows as
needed, on demand. Keep your code simple by relying on the association‐
list interface to find a key in a given bucket.
35. Hashtable diagnosis. A hash table is efficient only when its lists are short. But
if we want to confirm efficiency experimentally, the lengths of the lists are
hidden by the abstraction! To study a hash table’s performance, we have to
open up the abstraction barrier. In this exercise, I recommend exposing a
histogram of the lengths. For example, the histogram below depicts the state
of a hash table built by inserting the names of 10 Turing laureates into an
empty hash table:
0 |*********
1 |******
2 |**
The hash table has 17 buckets in all; 9 are empty, 6 have exactly one element,
and just 2 buckets have more than one element. The table contains 10 key‐
value pairs and so is about 60% full. The expected cost of a successful lookup
is determined by the average number of pairs in a nonempty bucket, which
is 1.25.
The next insertion brings the population to 11, triggering the grow operation.
The number of buckets increases to 23:
0 |**************
1 |*******
2 |**
36. Mutable and immutable sets. Using the data structure and invariants of your
37. Generic array functions. Revisit Exercise 1, and reimplement its functions to
work generically on any array whose elements are totally ordered. (If you
have completed Exercise 20, use module type ORD.)
38. Abstract tree traversals. Define a generic module Traversals, which should
export functions for traversing binary trees. It should take one parameter
Tree of this type:
600. hexercises.mcl 600i≡ 602 ▷
(module‑type BINARY‑TREE
(exports [abstype t]
[abstype value]
[empty? : (t ‑> bool)] ; observer
[the‑value : (t ‑> value)] ; observe only nonempty tree
[left : (t ‑> t)] ; subtree of nonempty tree
[right : (t ‑> t)])) ; subtree of nonempty tree
39. Sorting. Define a generic module Sort that takes as parameters an ARRAY
abstraction whose elements satisfy the ORD module type from Exercise 20.
The module should export one function sort, which sorts an array in place
(by mutating it).
(a) For various sizes of array, measure the cost of building a mutable heap
from an array in the style of the Heapsort module (Exercise 40), which
calls insert repeatedly.
(b) For various sizes of array, measure the cost of building a leftist heap
from an array, in a different style, by calling merge repeatedly: start
by making one‐element heaps, then merge pairs to make two‐element
heaps, then merge pairs to make four‐element heaps, and so on until
you have a single heap. This algorithm can be implemented fairly easily
by using an ArrayList as a queue.
(c) How large an array do you need before the repeated‐merge algorithm
beats the repeated‐insert algorithm? §9.12
(d) Revisit Exercise 40 (Heapsort), and implement two versions: one that Exercises
uses the priority queue based on a mutable array, and one that uses 601
the priority queue based on an immutable leftist heap. Measure which
heap provides a faster sort as the array to be sorted gets large.
42. Cost model of a hash table. An abstraction has both a semantics and a cost
model. For a hash table, the cost model includes constant‐time insertion.
Since an insertion can make the hash table grow, the appropriate bound is
not worst‐case cost; it is amortized cost: as N gets large, a sequence of N
insertions must have a cost proportional to N .
This aspect of hash‐table implementation can be hard to get right. A mis‐
take can change the cost of N insertions to O(N log N ) or even O(N 2 ).
In Exercise 34, all elements are reinserted every time the hash table grows,
and it grows O(log N ) times in total. But nevertheless, the total work is not
O(N log N ); it is O(N ).
(a) Suppose that at each growth step, N doubles. Some elements will be
reinserted for the log2 N th time, but half of the elements are rein‐
serted for the first time. Prove that the average number of reinsertions
is at most 2.
(b) Now suppose that at each growth step, N increases by a factor of 1.3.
Prove that the average number of reinsertions is bounded by a constant,
and find the constant.
(c) Demonstrate experimentally that as N gets large, the cost of inserting
N elements into the hash table is proportional to N . A plot of CPU time
versus N should resemble a straight line, but because the costs of gar‐
bage collection may vary with N , your plot may have some jitter.
To make your experiments as painless as possible, I recommend com‐
piling the Molecule interpreter with MLton or some other Standard ML
compiler that generates native machine code.
43. Refinements to a hash table. To find a key in a given bucket, the hash table in
Exercise 34 uses an association list. This convenience keeps the hash‐table
code simple: the hash table manages the array of buckets, and in any bucket,
it lets the association list search for the right key. But the convenience may
cost something at run time, especially if comparing keys for equality is ex‐
pensive. A production hash table might include these refinements:
• Along with each key‐value pair, a production hash table can store the
hash of each key, so it never needs to be recomputed.
• In lookup, insert, and delete, a production hash table can avoid com‐
paring keys except when they have the same hash. Hashes are small in‐
tegers and can be compared very quickly, whereas keys might be long
arrays (strings) or large data structures whose comparison takes much
longer. When there are multiple keys in a bucket, saving comparisons
might matter.
9 question.
(a) Which do you expect to be faster: the original hash table, or one with
Molecule, abstract the refinements? On what grounds? Justify your answer.
data types, and (b) Compared with the size of the Hash module, how much additional code
modules do you expect a refined version to require?
602 (c) Create a new module RefinedHash that implements the same interface
as Hash but incorporates the refinements listed above.
(d) Construct, if you can, a test program that exposes a performance dif‐
ference between the two different implementations of the hash table.
Explain your results.
44. Hybrid structures. Hash tables and arrays provide similar abstractions, but
with different cost models. But why should picking the right cost model be
up to the poor programmer? In this exercise, you make the computer do the
work. In the following abstraction, integers can index into a hash table or an
array, whichever seems better:
602. hexercises.mcl 600i+≡ ◁ 600
(module‑type HYBRID‑ARRAY
(exports [abstype t] ;; an array
[abstype elem] ;; one element of the array
[new : ( ‑> t)] ; creator
[at : (t int ‑> elem)] ; observer
[at‑put : (t int elem ‑> unit)] ; mutator
[foreach‑at : (t (int elem ‑> unit) ‑> unit)])) ; observer
Design and implement a generic module HybridArray, which takes one pa‐
rameter: a module Elem that exports a type t, a value default of type t,
a hash function, and an = function. An instance should have module type
HYBRID‑ARRAY, and your implementation should have these properties:
• Function new should return an array that maps every index to the
default element from the Elem parameter.
This sort of hybrid array provides the best of both worlds, but the imple‐
mentation may occasionally have to migrate keys from the hash table to the
internal array, or vice versa.
A design like this is used to implement the table abstraction in the program‐
ming language Lua.
45. Influence of base on naturalnumber performance. After completing the imple‐
mentation of natural numbers described in Exercise 49, experiment with dif‐
ferent values of the base b:
Compared with objects, abstract data types offer one great strength: a function can
easily inspect the representations of multiple arguments. This strength is demon‐
strated in the exercises below, starting with a simple set‐union problem and con‐
tinuing through progressively more ambitious forms of arithmetic.
Your Bignum module should take a module Natural of type NATURAL as a pa‐
rameter, and it should return a module that implements BIGNUM, like this:
605b. hbignum.mcl 605ai+≡ ◁ 605a
(generic‑module [Bignum : ([Natural : NATURAL] ‑‑m‑> BIGNUM)]
himplementation of signed integers (left as an exercise)i)
51. Better overloading. Molecule’s function overloading chooses the most re‐
cently overloaded type whose first argument matches the type of the func‐
tion’s first actual parameter. But Molecule could instead choose a function
based on the types and number of all the actual parameters.
For part (b), I wouldn’t try to compute a substitution—I would use a union‐
find algorithm.
An abstract data type is not the only way to hide the representation of data: a rep‐
resentation can also be hidden in an object. An object is a bundle of operations—
often called methods—that may share hidden state. An object interacts with other
objects by sending messages; each message activates a method on the receiving ob‐
ject. An object’s methods can see the representation of the object’s own state, but
not the representations of the states of other objects.
Grouping state with methods turns out to be unreasonably powerful. Perhaps
as a result, object‐oriented languages—or more precisely, procedural languages in‐
fused with object‐oriented ideas—have been popular since the 1990s. The popular
languages draw from ideas originally developed in Simula 67, Smalltalk‐80, and
Self, which collectively represent three decades of language design. Simula 67 (Ny‐
gaard and Dahl 1981) introduced objects and classes, which it layered on top of
Algol 60, a procedural language. Smalltalk‐80 simplified Nygaard and Dahl’s de‐
sign by eliminating the underlying procedural language; Smalltalk is purely object‐
oriented. Self (Ungar and Smith 1987) simplified Smalltalk’s design still further,
eliminating classes. For learning, the best of these languages is Smalltalk: Because
it is not layered on top of a procedural language, it won’t distract you with super‐
fluous features. And because it does have classes, it is easy to relate to such succes‐
sor languages as Objective C, C++, Modula‐3, Ruby, Java, Ada 95, and Swift—even
though most of these successors are actually procedural languages with object‐
oriented features.
Smalltalk is designed around one idea, simplified as much as possible, and
pushed to a logical extreme: everything is an object. In Smalltalk‐80, every value is
an object: for example, the number 12 is an object; every class is an object; the pro‐
gram is an object; the compiler is an object; every activation on the call stack is
609
an object; the programming environment is an object; every window is an object;
and even most parts of the hardware (display, keyboard, mouse) are modeled by
objects. Furthermore, conditionals and loops are implemented not by evaluating
special syntactic forms like if and while but by sending continuations to objects.
10 While µSmalltalk does not treat the interpreter and the hardware as objects, it does
ensure that every value and class is an object, and it does implement conditionals
and loops by sending continuations.
Smalltalk and
objectorientation 10.1 OBȷECT‐ORıENTED PROGRAMMıNG BY EXAMPLE
• Coordinate pairs, which represent locations and which can be added and
subtracted as vectors
• Shapes, which can be asked about their locations, moved to given locations,
and drawn on canvases
The object that receives a message is called the message’s receiver. On receiving
a message, an object activates the execution of a method; the method does some
internal computation and eventually replies with an answer. A method is a bit like a
function, and an object is a lot like a bundle of functions that share state—typically
mutable state. But in Smalltalk, unlike a procedural or functional programming
language, the caller cannot choose what function is called—a caller chooses only
what message to send. When the message is received, the method that is executed
is determined by the class of the receiver.
Syntactically, a message send has three parts: an expression that evaluates to
the receiver, the name of the message, and zero or more arguments:
exp ::= (exp messagename exp )
1
For Smalltalk, I say “execute” instead of “evaluate.” They mean the same thing, but “execute” carries
connotations of imperative features and mutable state, which are typical of Smalltalk.
In this syntax, as in Impcore’s function‐call syntax “(functionname exp ),” the
operation that is intended is named. But the name means something different, and
it is therefore written in a different place:
• In a procedural language like Impcore, the caller determines the code that is
executed and identifies it by naming the procedure, which is written first.
§10.1
• In an object‐oriented language like Smalltalk, the caller picks the name of the Objectoriented
message sent, and a method with that name is eventually executed, but the programming
receiver determines which one. That’s why the receiver is written first. by example
• In a functional language like µScheme, the caller doesn’t have to name the 611
code that is executed, but the semantics and syntax are like procedural se‐
mantics and syntax: the caller determines what function is executed, and
the function is written first.
Receiver‐first syntax is found not only in Smalltalk but also in C++, Java, JavaScript,
Ruby, Modula–3, Lua, Swift, and essentially all languages that emphasize object‐
oriented features. Most typically, the receiver is followed by a dot, the name of the
message, and arguments in parentheses.
Message sends by themselves are enough to write some interesting computa‐
tions, provided some interesting objects are available to send messages to. To il‐
lustrate, I send messages that draw the picture “ .” The messages are sent to
objects that represent classes, shapes, the picture, and a “canvas” on which the pic‐
ture is drawn. To start, I send the new message to the Circle class, which creates a
circle object.
611a. htranscript 611ai≡ 611b ▷
‑> (use shapes.smt) ; load shape classes defined in this section
‑> (val c (Circle new))
<Circle>
The Circle class, like every Smalltalk class, is also an object, and by default, send‐
ing new to a class object creates a new instance of that class. The word new does not
signal a special syntactic form the way it does in Java or C++; it’s a message name
like any other.
Next I send new to the Square class, creating a new square:
611b. htranscript 611ai+≡ ◁ 611a 611c ▷
‑> (val s (Square new))
<Square>
By default, a new shape has radius 1 and is located at the coordinate origin.
So both s and c are initially at the origin—and s needs to be moved to the right of c.
adjustPoint:to:
I send two messages:
621a
Circle 620
• Ask circle c for the location of its East “control point.” location: 621a
• Tell square s to adjust its position to put its West control point at that same Square 621b
location.
Message empty is sent to the Picture class, which answers a new object that repre‐
sents an empty picture. Message add: is then sent to that object, pic, with different
arguments. Each add: message answers the (modified) picture.
The picture is rendered using a canvas, which is created by sending the new
message to class TikzCanvas.
612c. htranscript 611ai+≡ ◁ 612b 622b ▷
‑> (val canvas (TikzCanvas new))
<TikzCanvas>
‑> (pic renderUsing: canvas)
\begin{tikzpicture}[x=4pt,y=4pt]
\draw (0,0)ellipse(1 and 1);
\draw (3,1)‑‑(1,1)‑‑(1,‑1)‑‑(3,‑1)‑‑cycle;
\draw (4,2)‑‑(3,0)‑‑(5,0)‑‑cycle;
\end{tikzpicture}
<Picture>
The arcane output is a program written for the TikZ package of the LATEX typesetting
system, which produces “ .”
The examples above send new to several class objects, send location: and
adjustPoint:to: to objects that represent shapes, send empty to the Picture class,
and send add: and renderUsing: to the resulting instance of Picture. How did I
know what messages to send? And how do the receiving objects know what to do
with them? Messages and their associated behaviors are determined by protocols
and class definitions, which are the subject of the next section.
In Smalltalk, we can send any message that the receiver understands. The mes‐
sage name is not looked up in an environment, as in Impcore, and it does not eval‐
uate to a value, as in Scheme. Instead, it is used by the receiver to determine what
code should be executed in response to the message, using the dynamic dispatch
algorithm described in Section 10.3.4 (page 631).
The messages that an object understands form the object’s protocol. Like a Mole‐
cule interface, a Smalltalk protocol describes an abstraction by saying what we can
do with it: what messages an object understands, what arguments are sent with
that message, and how the object responds.
Our first example protocols (Figure 10.1) are associated with class CoordPair,
which defines an abstraction of (x, y) coordinate pairs, or equivalently, two‐
dimensional vectors. In the pictures, these pairs represent locations. add:,
@Collection
• The first protocol is the class protocol for CoordPair. This protocol specifies B 644
@Picture 617
messages that can be sent to the CoordPair class, which is represented by adjustPoint:to:
an object named CoordPair. Sending withX:y: to the class results in a new 621a
object that is an instance of the class. location: 621a
Picture 617
• The second protocol is the instance protocol for CoordPair. This protocol renderUsing:
specifies messages that can be sent to any instance of the class. Sending * 617
TikzCanvas 619
multiplies the receiving vector by a scalar, while sending + or ‑ does vector
arithmetic. Sending print prints a textual representation of the instance.
The methods in the instance protocol are divided into three groups: access
to coordinates, arithmetic, and printing. In Smalltalk‐80, such groups were
called “message categories,” but today they are just “protocols”—so a class’s
instance protocol is a collection of smaller protocols.
The CoordPair abstraction is immutable, as we can tell from its protocol: no mes‐
sage mutates its receiver. In general, messages can be classified using the ideas
and terminology introduced in Section 2.5.2 (page 111), which are also used to clas‐
sify operations in a Molecule interface. In the class protocol, message withX:y: is
a creator. In the instance protocol, messages *, ‑, and + are producers. The abstrac‐
tion is immutable, so the protocols include no mutators. Messages x, y, and print
are observers.
The protocols for a class and its instances are determined by a class definition.
10 A class definition associates each message name (the interface) with a method (the
implementation). In µSmalltalk, a class definition is written with this syntax:
• A class definition names the new class and identifies its superclass.
Like instance variables, the instance methods and class methods of a superclass
are inherited by subclasses.
Our first class definition defines CoordPair, which implements the CoordPair
protocols (Figure 10.2, on the next page).
• Every new class has to have a superclass. Unless there is a reason to choose
something else, the superclass should be class Object. Class Object defines
and implements a protocol useful for all objects, including such operations
as printing, checking for equality, and several others. Class CoordPair in‐
herits this protocol and the corresponding methods, with two exceptions:
the inherited = and print methods are redefined, or, in object‐oriented ter‐
minology, overridden.
(method + (coordPair)
(CoordPair withX:y: (x + (coordPair x)) (y + (coordPair y))))
(method ‑ (coordPair)
(CoordPair withX:y: (x ‑ (coordPair x)) (y ‑ (coordPair y))))
)
The method definitions can be understood only once we understand the meanings
of the names they use. In Smalltalk, unlike in Molecule, access to information is
controlled by controlling the environment in which each method body is evaluated;
a method can access only what it can name.
Every method has access to global variables and to its named parameters, as in
Scheme or Molecule, plus any local variables that might be declared with locals.
And every method can use the special name self, which refers to the object re‐
left‑round
ceiving the message.4 (The name self cannot be assigned to; a method’s receiver
B 641
is mutated by mutating its instance variables.) Finally, every method has access Object B 637
to the instance variables of the receiver, which are referred to directly by name. print B 637
For example, in Figure 10.2, method initializeX:andY: mentions names x and y. right‑round
Unlike a function in a Molecule module, which has access to the representa‐ B 641
tion of every argument whose type is defined in that module, a Smalltalk method
has access only to the representation of the receiver; arguments are encapsulated.
A method has no access to the representation of any argument; all it can do with
an argument is send messages to it.
In Figure 10.2, the method definitions of class CoordPair are organized into
three groups.
4
Because the receiver does not appear on the list of a method’s formal parameters, referring to it re‐
quires a special name. Some object‐oriented languages use this instead of self; others provide syntax
by which the programmer can choose a name.
• Instances of class CoordPair are created by class method withX:y:, then
initialized by instance method initializeX:andY:. In the class method,
the self in (self new) refers to the class object, not to any instance. The
class, like all classes, inherits the new method from class Class; the new
10 method creates and answers a new, uninitialized object that is an instance
of class CoordPair. After sending new, class method withX:y: finishes
by sending the message initializeX:andY: to the new object. Method
initializeX:andY: initializes instance variables x and y, then answers
Smalltalk and
self—when message withX:y: is sent to the CoordPair class object, the fi‐
objectorientation
nal answer is the newly allocated, correctly initialized instance. This idiom is
616 common: a Smalltalk object is created by sending some message to its class,
and because the corresponding class method has no access to the instance
variables of the new object, it initializes the object using an instance method.
(The class object has no access to the instance variables of any other object,
not even one of its own instances.)
Method initializeX:andY: is commented as private, which means that it is
intended to be used only by other methods of the class, or of its subclasses.
Private methods are an essential part of object‐oriented programming, and
in different object‐oriented languages “private” is interpreted in slightly dif‐
ferent ways. In Smalltalk, private methods are purely a programming con‐
vention, which is not enforced by the language. Methods that initialize in‐
stance variables are often private.
• Every instance of class CoordPair provides two methods, x and y, which an‐
swer the values of the corresponding instance variables x and y.
initialized using the same idiom as in class CoordPair: class method empty calls pri‐
vate initialization method init, which initializes shapes with an empty list. (Class
List is predefined.)
To add a shape to a picture, method add: adds the shape to the shapes list.
To render a picture on a canvas, method renderUsing: prepares the canvas, draws
each shape, then notifies the canvas that drawing is complete.
Shapes are drawn by a loop, which is implemented using only message passing;
unlike Impcore or µScheme, Smalltalk does not have looping syntax. The loop is
new Answer a new instance of itself, which is not yet prepared for drawing.
10 startDrawing
stopDrawing
Prepare the receiver for drawing.
Tell the receiver that drawing is complete.
drawEllipseAt:width:height: aCoordPair aNumber aNumber
Smalltalk and The receiver issues drawing commands for an ellipse
objectorientation centered at the point with the given coordinates and
with the given width and height.
618
drawPolygon: aList The receiver issues drawing commands for a polygon
whose vertices are specified by the given list, which is
a list of CoordPairs.
(b) Instance protocol for canvases
implemented by sending the do: message to the list of shapes. The argument to the
do: message is a block, which is created by evaluating the special syntactic form
exp ::= [block ( argumentname ) exp ]
A canvas object hides a secret: how to draw ellipses and polygons, and how to sur‐
round them with the bureaucracy that the rendering engine requires. A simple can‐
vas protocol is shown in Figure 10.5, and class TikzCanvas, shown in Figure 10.6
on the facing page, encapsulates what I know about drawing simple pictures with
LATEX’s TikZ package. Like all good encapsulation, the design makes the code easy
to change: to switch to a different drawing language, like PostScript, I could simply
define a new class using the same protocol (Exercise 5).
The methods of class TikzCanvas are ugly—because µSmalltalk does not have
strings, the TikZ syntax can be emitted only by a sequence of print messages,
which are sent both to literal symbols and to predefined Unicode characters like
left‑curly.
Like the renderUsing: method of class Picture, the drawPolygon: method it‐
erates over a list, but here it is a list containing the vertices of the polygon. The
block—which is effectively the body of the loop—prints the coordinates of a vertex,
followed by the ‑‑ symbol.
5
The drawOn: message is part of the protocol for shapes; it is defined below.
619. hshape classes 615i+≡ ◁ 617 620 ▷
(class TikzCanvas
;;;;;;;;;;;; Encapsulates TikZ syntax
[subclass‑of Object]
(method startDrawing ()
('\begin print)
(left‑curly print) ('tikzpicture print) (right‑curly print) §10.1
(left‑square print) ('x=4pt,y=4pt print) (right‑square println)) Objectoriented
programming
(method stopDrawing ()
by example
('\end print)
(left‑curly print) ('tikzpicture print) (right‑curly println)) 619
'Center
'West 'East
radius
Our example picture includes three different shapes. And although each shape
draws itself differently, each shape handles the circumscribing square and its con‐
new The class answers a new instance of itself, whose radius is 1 and whose
center control point is at the coordinate origin.
(a) Class protocol for shapes
trol points in the same way. For that reason, all shapes can share an implemen‐
tation of the first part of the shape protocol: messages location:, locations:,
adjustPoint:to:, and scale: (Figure 10.7). Each shape needs its own implemen‐
tation of only one method: drawOn:. This kind of “sharing with a difference” is
exactly what inheritance is designed to provide.
• Each drawOn: method is defined in a unique subclass, which inherits the rep‐
resentation and the other methods of class Shape.
My design separates concerns: the superclass knows only how to use control points
to move and resize shapes, and a subclass knows only how to draw a shape.
Because each subclass defines only the drawOn: method, the subclass defini‐
tions are tiny, requiring little programming effort. For example, a circle is drawn
by drawing an ellipse whose width and height are both twice the radius of the cir‐
cumscribing square:
620. hshape classes 615i+≡ ◁ 619 621b ▷
(class Circle
[subclass‑of Shape]
;; no additional representation
(method drawOn: (canvas)
(canvas drawEllipseAt:width:height: center (2 * radius) (2 * radius)))
)
621a. hdefinition of class Shape 621ai≡
(class Shape
[subclass‑of Object]
[ivars center radius] ;; CoordPair and number
(class‑method new ()
((super new) center:radius: (CoordPair withX:y: 0 0) 1)) §10.1
(method center:radius: (c r) ;; private Objectoriented
(set center c)
programming
(set radius r)
by example
self)
621
(method location: (point‑name)
(center + ((point‑vectors at: point‑name) * radius)))
What’s new here is in the subclass‑of form: class Circle inherits from Shape, not add:,
from Object. So it gets the representation and methods of Shape. @Collection
As another example, class Square also inherits from Shape. Its drawOn: method B 644
draws the polygon whose vertices are the four corners of the square: @Picture 617
at: B 647
621b. hshape classes 615i+≡ ◁ 620 622a ▷ CoordPair 615
(class Square do: B 644
[subclass‑of Shape] drawEllipse‐
;; no additional representation At:width:
The locations: method converts the list of control‐point names into a list of co‐
ordinate pairs, which is then sent with drawPolygon:. Other shapes, including
Triangle, can be defined in the Exercises.
Both Circle and Square inherit from the superclass, Shape (Figure 10.8).
• A shape is created by sending message new to its class. Class method new
uses the now‐familiar pattern of creating a new object, then using a private
method to initialize it. But there is something different here: to allocate the
new object, the class method sends the new message to super, not to self.
Sending a message to super is a language feature that is described in detail in
Section 10.3.4 (page 632); for now, accept that sending new to super sends the
new message to class Shape, but in a way that dispatches to the new method
inherited from class Class. Sending new to self would dispatch to the rede‐
fined new method, which would cause infinite recursion.
10 • The location associated with a control point is computed by the location:
method, which finds a vector associated with the control point, multiplies
that vector by the radius, and adds the resulting vector to the center.
Smalltalk and To associate a vector with each control point, I define a global variable
objectorientation point‑vectors of class Dictionary (page 646).
622a. hshape classes 615i+≡ ◁ 621b
622
(val point‑vectors (Dictionary new))
(point‑vectors at:put: 'Center (CoordPair withX:y: 0 0))
(point‑vectors at:put: 'East (CoordPair withX:y: 1 0))
(point‑vectors at:put: 'Northeast (CoordPair withX:y: 1 1))
; ... six more definitions follow ...
The shapes examples uncover some of the essence of Smalltalk. In some ways,
Smalltalk resembles Impcore:
The shapes examples illustrate message sends, method dispatch, protocols, repre‐
sentation, object creation, and inheritance.
• A computation is implemented by a small army of objects that exchange mes at:put: B 647
sages. Each message is dispatched to a method, which is chosen at run time canvas 612c
based on the class of the receiver. As often as not, a method is executed for CoordPair 615
Dictionary
its side effects, like updating instance variables, or printing.
B S564
drawOn:,
• A method may be designated private, which means that its use should be lim‐ @Circle 620
ited to certain senders. For example, a message designated as private might @Shape 621a
be sent only by an object to itself, or by a class to its instances. In Smalltalk, @Square 621b
the designation is purely advisory: it records a programmer’s intent, but it Shape 621a
• The set of messages an object understands, together with the behavior that
is expected in response to those message, is the object’s protocol. Because
Smalltalk does not have a static type system, protocols are specified infor‐
mally. An object’s protocol is determined by the class of which the object
is an instance. The same protocol can be implemented by many different
classes, which are often related by inheritance. But they don’t have to be
related: an existing protocol can be implemented by an entirely new class
that does not inherit from any existing implementation, like PSCanvas (Ex‐
ercise 5).
10 Although Smalltalk does not have a static type system, it does have a way to
ensure that a message is accompanied by the correct number of arguments:
the number of arguments is determined by the name of the message. If the
name begins with a letter, like drawOn:, the message expects a number of ar‐
Smalltalk and guments equal to the number of colons in the name. If the name begins with
objectorientation a nonletter, like +, the message expects exactly one argument. Ensuring that
the number of arguments matches the number expected by the name costs
624 almost nothing, but like a type, it serves as documentation that is checked by
the compiler. And it catches mistakes at compile time. In proper Smalltalk‐
80 syntax, colons break the name into parts, which are interleaved among the
arguments. This concrete syntax, while unusual, leads to code that I enjoy
reading (Section 10.12).
• A new object is created by sending a message to its class, often the new mes‐
sage. The message dispatches to a class method, which allocates a new ob‐
ject and may also invoke an instance method to initialize the new object’s
instance variables. Occasionally a new object is created by evaluating some
other syntactic form, like a block expression, a literal integer, a literal sym‐
bol, or a class definition.
• Every class definition specifies a superclass, from which the new class inherits
both methods and instance variables. A class can be defined whose sole pur‐
pose is to be inherited from, like the Shape class. Such a class, which is not
intended for instantiation, is called an abstract class. A class whose methods
send the subclassResponsibility message is always abstract.
• In Smalltalk, the initial basis plays a greater role than in Impcore, Scheme,
or ML. Even basic control structures and data structures, like conditionals,
loops, and lists, are coded in the initial basis; they are not part of the lan‐
guage proper. To program effectively in Smalltalk, you must learn about the
predefined classes, like the lists and dictionaries used in the pictures exam‐
ple.
Systems based on abstract data types are “closed,” and can thereby be made re‐
10 liable. Systems based on objects and protocols are “open,” and can thereby be
made extensible.
Using abstract data types, as in Molecule, a representation is hidden in a mod‐
ule, and access is mediated by a module type that exports an abstract type T .
Smalltalk and
The line between “visible” and “hidden” is fixed:
objectorientation
• Only operations inside the module can see the representation of values of
626 type T .
• An operation inside the module can see the representation of any value
of type T , no matter where it comes from.
Once the module is sealed, the representation of T can’t be seen by new mod‐
ules. And new abstractions can’t be made compatible with T : a new module’s
abstract types, even if defined identically to T , cannot be used in a context that
expects T . New, compatible code can be added only by opening up an existing
module and editing it; the interface used to seal the module might also need
editing. Code added outside the module can’t extend its capabilities but also
can’t make it fail.
The representation of an object can be seen by new code, provided that code
appears in methods that are defined in a new subclass. And new abstractions
can be made compatible with old ones: if a new object implements an old pro‐
tocol, then it can be used in any context that expects the old protocol. An object‐
oriented system can be extended without editing existing code.
Objects and abstract data types support data abstraction in different ways, and
these differences lead to differences in client code.
• In Molecule, an abstraction is specified by a named abstract data type, and
operations of the abstraction work only with values of that type. In Smalltalk,
an abstraction is specified by a protocol, and operations of the abstraction
work with objects of any class that implements the protocol. If an object
implements the fraction protocol, then it walks like a fraction, talks like a
fraction, and exchanges messages in the protocol for fractions—so even if its
definition doesn’t inherit from class Fraction, for programming purposes
§10.3
it’s a fraction. Because an object’s behavior is what matters, this property is
The µSmalltalk
called behavioral subtyping, or by Ruby programmers, “duck typing.”
language
• In a Smalltalk method, self is the only object whose representation is acces‐ 627
sible. Objects sent as arguments must implement the expected protocols, but
a method defined on the receiver cannot see their representations. Methods
that wish they could see arguments’ representations, like + and ‑ on class
CoordPair, have to send messages to those arguments instead—usually mes‐
sages like x and y. In Molecule, the analogous operations get to see all rele‐
vant representations directly.
• A Smalltalk class can specify not a single abstraction, but a family of related
abstractions. Members of the family are implemented by subclasses. Ex‐
amples shown in this chapter include shapes, numbers, and “collections.”
Additional examples found only in full Smalltalk include input streams and
user‐interface widgets.
Compared with Molecule modules, Smalltalk classes are more flexible and easier
to reuse, but operations that wish to see multiple representations are harder to im‐
plement. And unlike Molecule, Smalltalk does not have a type checker, so it cannot
guarantee that every operation is given abstractions that it knows how to work with.
To understand how these trade‐offs work, study the design and implementation of
the Collection hierarchy, shown in Sections 10.4.5, 10.7.1, 10.7.2, and 10.9.
The concrete syntax of µSmalltalk is shown in Figure 10.9 on the next page. Three
of the first four expression forms are shared with Impcore and µScheme: variables,
set, and begin. All three languages also have literal expressions, but each one has a
different set—µSmalltalk has integer literals and symbol literals as in µScheme, but
in place of list literals, µSmalltalk has array literals, and it has no Boolean literals.
µSmalltalk’s remaining expression forms are found neither in Impcore nor in
µScheme. Most important, instead of function application µSmalltalk has message
send. In a send, the message, like an Impcore function, is identified by name.6 Ev‐
ery message name has an arity, which is the number of arguments that the message
expects. If the messagename begins with a nonletter, its arity is 1. If the message
name begins with a letter, its arity is the number of colons in the name.
In addition to message send and to the shared forms, a µSmalltalk expression
can be a block, which is like a lambda abstraction in Scheme. A block specifies
6
Impcore’s functions are not values, but µScheme’s functions are values. Are µSmalltalk’s functions
values? It depends what you mean by “function.” If you mean “block,” then yes, blocks are values. If you
mean “message name,” then no, message names are not values. (In Smalltalk‐80, the message name is
called a message selector, and it can be given as a value with the perform: message.)
exp ::= literal
| variablename
| (set variablename
exp)
| (begin exp )
10 | (exp messagename
exp )
| [block
( argumentname ) exp ]
| { exp }
Smalltalk and | (return exp)
objectorientation | (primitive primitivename exp
)
| (compiled‑method (formals) [locals locals] exp )
628
literal ::= integerliteral
| 'symbolname
| '( arrayelement )
arrayelement
::= integerliteral
| symbolname
| ( arrayelement )
formals ::= formalparametername
locals ::= localvariablename
methoddefinition
::= (method methodname (formals) [locals locals] exp )
| (class‑method methodname (formals) [locals locals] exp )
numeral ::= token composed only of digits, possibly prefixed with a plus
or minus sign
any *name
::= token that is not a bracket, a numeral, or one of the “re‐
served” words shown in typewriter font
10.3.2 Values
Every object is an instance of some class, which eventually inherits from class
Object. Therefore every object responds to the messages in Object’s protocol.
These messages include =, ==, println, class, and many others. They are a bit
like predefined functions in other languages, with an important difference: they
can be redefined.
7
Smalltalk‐80 writes blocks in square brackets, but in this book, square brackets work just like round
brackets—you mix square and round brackets in whatever way you find easiest to read.
The undefined object, nil
The object nil is the sole instance of class UndefinedObject. It conventionally rep‐
resents a bad, missing, undefined, or uninitialized value. Smalltalk’s nil is quite
10 different from the nil used in some dialects of Lisp, Scheme, Prolog, and ML,
which represents the empty list.
Class objects
Every class definition creates a class object, which itself has a class that inherits
(through intermediaries) from class Class. The class object can respond to new
and possibly to other class‐dependent initialization messages. And the class object
is an instance of another class: The object representing class C is an instance of C’s
metaclass. (It is the only instance of C’s metaclass.) The metaclass holds C’s class
methods. In more detail, if an object c is an instance of class C, the messages that
c can respond to are those defined on C’s class object. Similarly, the messages that
class C can respond to are those defined on C’s metaclass. Because every metaclass
inherits from class Class, which defines new, every class object can respond to a
new message.
In addition to new, class Class defines other methods that enable client code
to interrogate or alter a class. For example, printProtocol prints all the messages
that a class and its instances know how to respond to; superclass answers the class
object of the receiver’s superclass, if any; compiledMethodAt: answers a method;
and addSelector:withMethod: can add a new method or change an existing one.
Such reflective facilities, which enable the language to manipulate itself, are even
more numerous in full Smalltalk‐80, in which you can learn the names of meth‐
ods and instance variables, execute methods by name, create new subclasses, and
much more, all by sending messages. The reflection methods are so powerful that
Smalltalk‐80’s class browsers and debuggers are implemented in Smalltalk itself.
10.3.3 Names
Just as Impcore has distinct name spaces for functions and values, µSmalltalk has
distinct name spaces for message names and variables. The name of a message
is resolved dynamically: until a message is sent, we can’t tell what method will be
executed to respond to it. The name of a variable is resolved statically; that is, we
can tell by looking at a class definition what each variable name in each method
must stand for. In particular, the name of a variable stands for one of the following
possibilities:
Except when a message is sent to super, an object of class C responds to any mes‐
sage for which a method is defined in C —or in any of C ’s ancestors. When an object
of class C receives a message with selector m, it looks in C ’s definition for a method
named m. If that fails, it looks in the definition of C ’s superclass, and so on. When
it finds a definition of a method for m, it executes that method. If it reaches the
top of the hierarchy without finding a definition of m, an error has occurred: the
message is not understood.
For example, when location: is sent to an object of class Circle, class Circle
does not define a location: method, but Circle’s superclass, class Shape, does de‐
fine a location: method, so the message is dispatched to the location: method
defined on class Shape. On the other hand, if drawOn: is sent to an object of
10 class Circle, it is immediately dispatched to the drawOn: method defined on class
Circle—class Shape is not consulted.
Unlike an ancestor’s instance variable, an ancestor’s method can be redefined.
When a method is defined in C and also in an ancestor, and the relevant message
Smalltalk and
is sent to an object of class C , it is C ’s definition of the method that is executed.
objectorientation
This method‐dispatch rule produces outcomes that might surprise you, so let’s look
632 at a contrived example.
Suppose classes B (the superclass or ancestor) and C (the subclass) are defined
as follows:
632a. htranscript 611ai+≡ ◁ 622b 632b ▷
‑> (class B [subclass‑of Object]
(method m1 () (self m2))
(method m2 () 'B))
‑> (class C [subclass‑of B]
(method m2 () 'C))
3. Class C defines m2, so the message send dispatches to that definition. And
class C’s m2 method answers the symbol C.
This example illustrates a crucial fact about Smalltalk: the search for a method begins
in the class of the receiver. Many more examples of method dispatch appear through‐
out the chapter.
Does method dispatch always begin in the class of the receiver? Almost always.
A message sent to super is dispatched in a special way. The message is sent to
self, but the method search begins in the superclass of the class in which the message
to super appears. That is, unlike the starting place for a normal message send, the
starting place for a message to super is statically determined, independent of the
class of the receiver self. This behavior guarantees that a particular method from
the superclass will be executed.
We typically send to super when we wish to add to the behavior of an inherited
method, not simply replace it. The most common examples are class methods that
initialize objects, like method new in class Shape. A new method is defined on every
class, and a properly designed new method not only allocates a new object but also
establishes the private invariants of its class. Simply sending new to self executes
only the new method defined on the class of the object being created. But if there are
invariants associated with the superclass, those invariants need to be established
too. All the invariants can be established by the following idiom: each subclass
sends new to super, establishing the superclass invariants, then executes whatever
§10.3
additional code is needed to establish subclass invariants. And when a subclass has
The µSmalltalk
no invariants of its own, it can take a shortcut and simply inherit new.
language
Below, Section 10.4 dives into Smalltalk’s initial basis, starting with the messages
that every object understands. But some of those messages implement equality
tests, which deserve a section of their own.
The issue of when two objects should be considered equal is one we have
danced around since Chapter 2. Equality is so central, and yet so seldom addressed
well, that when you encounter a new programming language, it is one of the first
things you should look at. This section will tell you what to look for.
Almost all languages support constant‐time equality tests on simple, scalar val‐
ues like integers and Booleans. But equality tests on more structured data, like
records, sums, and arrays, can be done in more than one way—and there is no
single right way (Noble et al. 2016). Moreover, when a language supports data ab‐
straction, it is all too easy to test equality in the wrong way, by violating abstraction.
To expose some of the issues, let’s review some designs you’ve already seen.
• C has only one form of equality, and it applies only to scalar data (integers,
Booleans, floating‐point numbers, and similar) and to pointers. Two point‐
ers are equal if and only if they point to the same memory; viewed at a higher
level of abstraction, C’s == operator tests for object identity. (A well‐known be‐
ginner’s mistake is to use == to compare strings for equality; that comparison
demands strcmp.) Structured data like structs and unions cannot be com‐
pared for equality, and C famously does not have arrays—only pointers and
memory.8
• µScheme has two forms of equality, written = and equal?. The = operator
works only on scalar data; given two pairs, it always returns #f—according
to µScheme’s =, a cons cell is not even equal to itself. The equal? predicate,
on the other hand, provides structural similarity; it returns #t whenever two
values have the same structure, even if the structure is arbitrarily large.
Full Scheme, in which cons cells are mutable, has a more principled design.
Function equal? acts as in µScheme, providing structural similarity. Func‐
tion eqv? provides object identity, and it compares not only scalar data but Object B 637
also cons cells, vectors, and procedures. And function eq? provides object
identity on structured data, but on numeric data and character data it is more
efficient if less predictable than eqv?.
Notions of equivalence
To get equality right, Smalltalk uses the same methodology as Molecule. The meth‐
odology is based on a central idea of programming‐language theory, called observa
tional equivalence. A pointy‐headed theorist would say that two things are observa‐
tionally equivalent if there is no computational context that can distinguish them.
For a programmer who doesn’t normally talk about computational contexts, the
idea makes more sense as a programming principle:
Two values should be considered equal if no program can tell them apart.
• A mutable abstraction, like a dictionary, should compare equal only with it‐
self. That’s because two different mutable objects can be told apart by mu‐
tating one and seeing that the other doesn’t change. Therefore, on mutable
data, equality must be implemented as object identity.
• Finally, in a language like Smalltalk, even though reflection can breach the
abstraction barrier it should not be used to distinguish objects that would
otherwise be indistinguishable.
Two objects are considered equivalent if, without mutating either object or
using reflection, client code cannot tell them apart.
In other words, two objects are considered equivalent if at this moment they rep‐
resent the same abstraction. This is the equivalence that is used by µSmalltalk’s
check‑expect and implemented by the = method.
To implement = correctly on each class requires an understanding of the class’s
representation invariant and abstraction function (Section 9.6, page 545). But as a
default, a conservative approximation is defined on all objects:
635a. hmethods of class Object 634i+≡ ◁ 634 655a ▷
(method = (anObject) (self == anObject))
(method != (anObject) ((self = anObject) not))
This default is conservative in the sense that if it says two objects are equivalent,
they really are. If two objects are equivalent but not identical, however, the default
= will report, incorrectly, that they are different.
To illustrate the distinction between identity and equivalence, the following exam‐
ple uses lists. The two lists ns and ms are built differently:
635b. htranscript 611ai+≡ ◁ 632b 636 ▷
‑> (val ns (List new))
List( ) addFirst: B 649
‑> (ns addFirst: 3) addLast: B 649
‑> (ns addFirst: 2) List B 649
‑> (ns addFirst: 1)
‑> ns
List( 1 2 3 )
‑> (val ms (List new))
List( )
‑> (ms addLast: 1)
‑> (ms addLast: 2)
‑> (ms addLast: 3)
‑> ms
List( 1 2 3 )
Why no mutation?
10 be considered different only if client code can tell them apart. But why shouldn’t
the client code be allowed to mutate? In isolation, this restriction makes little
sense. But in the context of a whole language design, the restriction makes prag‐
matic sense: if you already have object identity, and if you permit client code to
Smalltalk and mutate objects, then client‐code observation gives you nothing new. A second
objectorientation form of observational equivalence is useful only if it gives different results.
636
Lists ns and ms aren’t the same object, but because they represent the same se‐
quence of values, they are equivalent:
636. htranscript 611ai+≡ ◁ 635b 637 ▷
‑> (ns == ms)
<False>
‑> (ns = ms)
<True>
Much of Smalltalk’s power comes from its impressive predefined classes; using
and inheriting from these classes is an essential part of effective Smalltalk pro‐
gramming. By the standards of Smalltalk‐80, µSmalltalk’s hierarchy of predefined
classes is small, but by the standards of this book, it is large—sufficient to help
you learn what Smalltalk programming is like, but too large to digest all at once.
So you’ll start by digesting just the abstractions that the predefined classes repre‐
sent and the protocols they understand (this section, 10.4). That will be enough
so that you can use the classes. Then to develop good µSmalltalk skills, you can
go on to study key techniques as illustrated by implementations of some predefined
classes (Sections 10.6 to 10.9).
Every µSmalltalk object responds to the messages in Figure 10.10 (on the next
page), which are defined on the primitive class Object. Messages = and != test
for equivalence, while == and !== test for identity. Messages isNil and notNil test
for “definedness.”
The print message is used by the µSmalltalk interpreter itself to print the val‐
ues of objects; println follows with a newline.
The error: message is used to report errors. The subclassResponsibility
message, which also reports an error when sent, is used to mark a class as abstract
(page 622); a method that sends subclassResponsibility is sometimes called a
“marker method.” Finally, the leftAsExercise message is sent by methods that
are meant for you to implement, as exercises.
The three messages class, isKindOf:, and isMemberOf: enable introspection
into objects. Message class gives the class of a receiver, and messages isKindOf:
and isMemberOf: test properties of that class. For example, the literal integer 3 is
== anObject Answer whether the argument is the same object
as the receiver.
!== anObject Answer whether the argument is not the same
object as the receiver.
= anObject Answer whether the argument should be
considered equal to the the receiver, even if they
are not identical. §10.4
The initial basis
!= anObject Answer whether the argument should be
of µSmalltalk
considered different from the receiver.
isNil Answer whether the receiver is nil. 637
an object from some proper subclass of Number, and the literal symbol '3 is not a
number at all:
637. htranscript 611ai+≡ ◁ 636 639a ▷
‑> (3 isKindOf: Number)
<True>
‑> (3 isMemberOf: Number)
<False>
‑> ('3 isKindOf: Number) Number B 663
<False>
Because every class is also an object, a class can answer messages. Every class ob‐
ject inherits from Class, and it responds to the protocol in Figure 10.11 (as well as to
every message in the Object protocol). Parts of the protocol approximate capabili‐
ties that a full Smalltalk system provides in a graphical user interface: messages
printProtocol and printLocalProtocol are meant for interactive exploration,
and message addSelector:withMethod enables you to modify or extend built‐in
classes.
object; the expression inside the brackets is not evaluated until the block is sent
the value message.
639a. htranscript 611ai+≡ ◁ 637 639b ▷
‑> (val index 0)
‑> {(set index (index + 1))}
<Block>
‑> index
0
‑> ({(set index (index + 1))} value)
1
‑> index
1
Just like any object, a block can be assigned to a variable and used later.
639b. htranscript 611ai+≡ ◁ 639a 640a ▷
‑> (val incrementBlock {(set index (index + 1))}) value B 641
<Block>
‑> (val sumPlusIndexSquaredBlock {(sum + (index * index))})
<Block>
‑> (val sum 0)
0
‑> (set sum (sumPlusIndexSquaredBlock value))
1
‑> (incrementBlock value)
2
‑> (set sum (sumPlusIndexSquaredBlock value))
5
Parameterless blocks are used primarily as continuations, to implement loop
bodies, exception handlers, or conditional expressions, for example. In other lan‐
guages, including Scheme, a conditional expression requires a special syntactic
form, like (if e1 e2 e3 ). This form is evaluated specially by the interpreter, which
10 first evaluates e1 , then evaluates either e2 or e3 , as needed. In µSmalltalk, as in full
Smalltalk‐80, if does not have its own syntactic form; it is coded using message
passing and continuations.9 In µSmalltalk, writing (e1 ifTrue:ifFalse: e2 e3 )
asks the interpreter to evaluate all three expressions e1 , e2 , and e3 before dispatch‐
Smalltalk and
ing to method ifTrue:ifFalse: on the object that e1 evaluates to. If e2 and e3 are
objectorientation
meant to be evaluated conditionally, their evaluation must be delayed by putting
640 them in blocks: (e1 ifTrue:ifFalse: {e2 } {e3 }). The effect of the delay is illus‐
trated by this transcript:
640a. htranscript 611ai+≡ ◁ 639b 640b ▷
‑> ((sum < 0) ifTrue:ifFalse: {'negative} {'nonnegative})
nonnegative
‑> ((sum < 0) ifTrue:ifFalse: 'negative 'nonnegative )
Run‑time error: Symbol does not understand message value
Method‑stack traceback:
In predefined classes, line 37, sent `value` to an object of class Symbol
In standard input, line 154, sent `ifTrue:ifFalse:` to an object of class False
On the first line, (sum < 0) produces the Boolean object false. Sending message
ifTrue:ifFalse: to false causes false to send value to the block {'nonnegative},
which answers the symbol 'nonnegative, which is the result of the entire expres‐
sion. On the second line, false sends value to the symbol 'nonnegative, which
results in an error message and a stack trace.
The ifTrue:ifFalse: message is an example of continuationpassing style (Sec‐
tion 2.10, page 136). Two blocks—the continuations—are passed to a Boolean. The
true object continues by executing the first block; false continues by executing
the second. Message ifTrue:ifFalse: and the other continuation‐passing mes‐
sages of the Booleans are shown in the top half of Figure 10.12 (on the previous
page). In addition to classic conditionals, these messages also include short‐circuit
and: and or:. The bottom half of the figure shows messages that implement simple
Boolean operations.
Like conditionals, loops are implemented in continuation‐passing style. But a
loop is implemented not by sending a message to a Boolean, but by sending a mes‐
sage to a block, which holds the condition. The condition must be a block because
it must be re‐evaluated on each iteration of the loop.
640b. htranscript 611ai+≡ ◁ 640a 645a ▷
‑> ({(sum < 10000)} whileTrue: {(set sum (5 * sum)) (sum println)})
25
125
625
3125
15625
nil
When the loop terminates, the whileTrue: method answers nil, which the inter‐
preter prints.
A parameterless block also understands the messageTrace message, which tells
the interpreter to show every message send and response during the evaluation of
9
In fact, implementations of Smalltalk‐80 have traditionally “open coded” conditionals, preventing
user‐defined classes from usefully defining method ifTrue:ifFalse: (ANSI 1998). The Self language
manages efficiency without such hacks (Chambers and Ungar 1989; Chambers 1992).
value Evaluate the receiver and answer its value.
value: anArgument Allocate a fresh location to hold the
argument, bind that location to the receiver’s
formal parameter, evaluate the body of the
receiver, and answer the result.
value:value: arg1 arg2
value:value:value: arg1 arg2 arg3
§10.4
The initial basis
value:value:value:value: arg1 arg2 arg3 arg4
of µSmalltalk
Like value:, but with two, three, or four
arguments. 641
whileTrue: bodyBlock Send value to the receiver, and if the
response is true, send value to bodyBlock
and repeat. When the receiver responds
false, answer nil.
whileFalse: bodyBlock Send value to the receiver, and if the
response is false, send value to bodyBlock
and repeat. When the receiver responds
true, answer nil.
messageTrace Send value to the receiver, and print a trace
of every message send and reply until the
receiver answers.
messageTraceFor: anInteger Send value to the receiver, and print a trace
of the first anInteger message sends and
replies.
the block’s body. The number of sends shown can be capped by instead sending
messageTraceFor: (Figure 10.13).
A block may also take parameters. Such a block must be written using the block
keyword, like this block from the drawPolygon: method of class TikzCanvas:
(coord‑list do: [block (pt) (pt print) ('‑‑ print)])
A block that expects one parameter is activated by sending it the value: mes‐
sage with one argument. A block that expects two parameters is activated by
value:value: with two arguments, and so on up to four parameters.
All languages need ways of grouping individual data elements into larger collec‐
tions. Some form of collection is usually found in a language’s initial basis; for
10 example, Scheme has lists and Molecule has arrays. µSmalltalk predefines four
useful collections: sets, lists, arrays, and dictionaries. And defining new collec‐
tions is exceptionally easy, because any new collection can inherit most of its code
from class Collection or from one of its subclasses.
Smalltalk and
Collections are also found in Smalltalk‐80, whose collection classes are similar
objectorientation
to µSmalltalk’s classes but are structured somewhat differently (Section 10.12.3,
642 page 705). µSmalltalk’s collection hierarchy is inspired by Budd (1987).
A collection is an abstraction, like the abstractions written in Molecule in Chap‐
ter 9. But a collection isn’t a type or a type constructor; Smalltalk provides data
abstractions using objects, not abstract data types, and object‐oriented abstrac‐
tions work differently. In Smalltalk, a collection is an object that responds to the
collection protocol (Figure 10.15, page 644); whether it has anything to do with the
Collection class is a matter of convenience. The protocol’s core operations are
iteration, addition, and removal:
• A collection must respond to the do: message by iterating over its contents.
When a message of the form (collection do: [block (x) body]) is sent, the
receiver must respond by evaluating body once for each item x that it con‐
tains. A do: method acts like the µScheme app function, but it works on any
collection, not just on a list.
Collection
Set KeyedCollection
Dictionary SequenceableCollection
List Array
• Class List implements a list, which is a sequence that can grow or shrink.
Unlike a list in Scheme or ML, a Smalltalk list is mutable, and it can add or
remove elements at either its beginning or its end, in constant time. Class
List is a concrete subclass that can be used to instantiate lists.
The collection protocols are described in detail in Figures 10.14 (class protocol)
and 10.15 (instance protocol).
• The instance methods in the first group are mutators; they define ways of
adding and removing elements. In µSmalltalk, a mutator typically answers
the receiver, which makes it easy to send several mutation messages to the
same object in sequence.
• The instance methods in the second group are observers; they define ways
of finding out about the elements in a collection. The includes: and
occurrencesOf: observers ask about elements directly; the detect: and
detect:ifNone: observers ask for any element satisfying a given predicate,
which is typically represented as a block. The = observer asks if two collec‐
tions contain the same objects (according to =).
add: newObject Include the argument, newObject, as one of the
elements of the receiver. Answer the receiver.
addAll: aCollection Add every element of the argument to the
10 remove: oldObject
receiver; answer the receiver.
Remove the argument, oldObject, from the
receiver. If no element is equal to oldObject,
Smalltalk and report an error; otherwise, answer the receiver.
objectorientation remove:ifAbsent: oldObject exnBlock
Remove the argument, oldObject, from the
644 receiver. If no element is equal to oldObject,
answer the result of evaluating exnBlock;
otherwise, answer the receiver.
removeAll: aCollection Remove every element of the argument from the
receiver; answer the receiver or report an error.
isEmpty Answer whether the receiver has any elements.
size Answer how many elements the receiver has.
includes: anObject Answer whether the receiver has anObject.
occurrencesOf: anObject Answer how many of the receiver’s elements are
equal to anObject.
detect: aBlock Answer the first element x in the receiver for
which (aBlock value: x) is true, or report an
error if none.
detect:ifNone: aBlock exnBlock
Answer the first element x in the receiver for
which (aBlock value: x) is true, or answer
(exnBlock value) if none.
= aCollection Answer whether the contents of the receiver are
equivalent to the contents of the argument.
do: aBlock For each element x of the collection, evaluate
(aBlock value: x).
inject:into: aValue binaryBlock
Evaluate binaryBlock once for each element in
the receiver. The first argument of the block is
an element from the receiver; the second
argument is the result of the previous evaluation
of the block, starting with aValue. Answer the
final value of the block.
select: aBlock Answer a new collection like the receiver,
containing every element x of the receiver for
which (aBlock value: x) is true.
reject: aBlock Answer a new collection like the receiver,
containing every element x of the receiver for
which (aBlock value: x) is false.
collect: aBlock Answer a new collection like the receiver,
containing (aBlock value: x) for every
element x of the receiver.
• The instance methods in the final group are producers; given a collection, §10.4
a producer makes a new collection without mutating the original. Methods The initial basis
select: and collect: correspond to the µScheme functions filter and map, of µSmalltalk
which are also defined in ML. 645
Collection is an abstract class, so a client should not send a new, with:, or
withAll: message directly to Collection—only to one of its concrete subclasses.
The simplest subclass of Collection is Set; to get a feel for sets and for the
Collection protocol, look at this transcript:
645a. htranscript 611ai+≡ ◁ 640b 645b ▷
‑> (val s (Set new))
Set( )
‑> (s size)
0
‑> (s add: 2)
Set( 2 )
‑> (s add: 'abc)
Set( 2 abc )
‑> (s includes: 2)
<True>
‑> (s add: 2)
Set( 2 abc )
Remember that when a message is sent to a receiver, the method has access not
only to the values of the arguments but also to the receiver. For example, the add:
method takes only one argument, the item to be added, but it also has access to the
collection to which the item is added.
Collection methods can operate on collections of mixed kinds. For example,
I build a set s from an array, and then I use the set’s addAll: method to add the
elements of another array.
645b. htranscript 611ai+≡ ◁ 645a 645c ▷
‑> (set s (Set withAll: '(1 2 3 1 2 3)))
Set( 1 2 3 )
‑> (s addAll: '(1 2 3 a b c d e f))
Set( 1 2 3 a b c d e f )
‑> (s includes: 'b)
<True>
Number B 663
This kind of mixed computation, where I pass an array as an argument to a set op‐
Set B S563
eration, is not possible using abstract data types. But using objects, it’s easy: the
addAll: method treats the argument object abstractly. As long as the argument
responds to the Collection protocol, addAll: doesn’t care what class it’s an in‐
stance of. The addAll: method interacts with the argument only by sending it the
do: message.
Other messages that work with any collection include removeAll: and reject:.
645c. htranscript 611ai+≡ ◁ 645b 648 ▷
‑> (s removeAll: '(e f))
Set( 1 2 3 a b c d )
‑> (val s2 (s reject: [block (x) (x isKindOf: Number)]))
Set( a b c d )
Object‐orientation also provides an effortless form of polymorphism. In the
example, set s is initialized by sending Set the withAll: message with array ar‐
gument '(1 2 3 1 2 3), which contains duplicate elements. The implementation of
withAll:, which is shared by multiple collection classes, eliminates the duplicates.
10 It identifies duplicates by sending the = message to individual elements. Because
the = message is dispatched dynamically, it automatically has the right semantics
for comparing elements, no matter what classes the elements are instances of.
In Scheme, by contrast, getting equality right requires either passing an equality
Smalltalk and
function as a parameter, storing it in a data structure, or capturing it in a closure
objectorientation
(Section 2.9, page 131). But in Smalltalk, as in every object‐oriented language, an
646 object already acts like a closure—it is data bundled with code—and the bundle in‐
cludes the right version of =. No additional coding is required.
• The at:put: message updates the receiver; (kc at:put: key value) modi‐
fies collection kc by associating value with key.
• The observers at: and keyAtValue: answer the value associated with a key
or the key associated with a value. The observer at:ifAbsent: can be used
to learn whether a given key is in the collection. (To learn whether a given
value is in a collection, use the observer includes: from the Collection pro‐
tocol.) Each of these observers works with a key that is equivalent to the key
originally used with at:put:—identical keys are not needed.
Observer at: and mutator at:put: are the most frequently used of the new mes‐
sages. Old messages size, isEmpty, and includes: are also used frequently.
KeyedCollection is an abstract class; a client should create instances of sub‐
classes only. Its simplest subclass is Dictionary, which represents a collection as a
list of associations. Class Dictionary assumes only that its keys respond correctly
to the = message. A more efficient subclass might assume that keys respond to a
hash message or a comparison message, which would enable it to use a hash table
or a search tree (Exercises 28 and 29).
at:put: key value Modify the receiver by associating key
with value. (May add a new value or §10.4
replace an existing value.) Answer the The initial basis
receiver. of µSmalltalk
removeKey: key Modify the receiver by removing key 647
and its associated value. If key is not in
the receiver, report an error; otherwise,
answer the value associated with key.
removeKey:ifAbsent: key aBlock Modify the receiver by removing key
and its associated value. If key is not in
the receiver, answer the result of
sending value to aBlock; otherwise,
answer the value associated with key.
at: key Answer the value associated with key, or
if there is no such value, report an error.
at:ifAbsent: key aBlock Answer the value associated with key,
or the result of evaluating aBlock if
there is no such value.
includesKey: key Answer true if there is some value
associated with key, or false otherwise.
keyAtValue: value Answer the key associated with value,
or if there is no such value, report an
error.
keyAtValue:ifAbsent: value aBlock
Answer the key associated with value,
or if there is no such value, answer the
result of evaluating aBlock.
associationAt: key Answer the Association in the
collection with key key, or if there is no
such Association, report an error.
associationAt:ifAbsent: key exnBlock
Answer the Association in the
collection with key key, or if there is no
such Association, answer the result of
evaluating exnBlock.
associationsDo: aBlock Iterate over all Associations in the
collection, evaluating aBlock with each
one.
Sequenceable collections
Arrays An array is a sequence that cannot change size, but that implements at:
and at:put: in constant time. Arrays are found in many programming languages;
in µSmalltalk, every array is one‐dimensional, and the first element’s key is 0.
(In Smalltalk‐80, the first element’s key is 1.)
An instance of Array responds to the messages of the SequenceableCollection
protocol. (Because an array cannot change size, it responds to add: and remove:
with errors.) The Array class responds to the messages of the Sequenceable‐
Collection class protocol, and also to message new:, which expects a size:
To define a new subclass of Collection might seem like an enormous job; after all,
Figure 10.15 shows a great many messages, an instance of any subclass must un‐
derstand. But the Collection class is carefully structured so that a subclass need
implement only four of the methods in Figure 10.15: do:, to iterate over elements,
add:, to add an element, remove:ifAbsent:, to remove an element, and =, to com‐
pare contents. The details appear in Section 10.7.1.
Collections make up a large part of µSmalltalk’s initial basis. Much of the rest of
the basis deals with numbers. The numeric classes illustrate the same inheritance
techniques as the collection classes: each concrete subclass defines a minimal set
of representation‐specific methods like +, *, and negated, and they all share inher‐
ited implementations of generic methods like ‑, isNegative, and abs. The most
important numeric classes fit into this hierarchy:
add: B 644
Object at: B 647
Magnitude
Number
of search trees, sorted collections, and so on. Class Number supports not only com‐
parisons but also arithmetic, plus other operations that can be performed only on
numbers: a Number can be asked about its sign; it can be asked for a power, square,
or square root; and it can be coerced (converted) to be an integer, a fraction, or a
floating‐point number. Protocols for both Magnitude and Number are shown in Fig‐
ure 10.20 (on the facing page).
In Smalltalk‐80, the predefined magnitudes include dates, times, and charac‐
ters; in µSmalltalk, the predefined magnitudes include only natural numbers and
the Number classes. Other magnitudes can be defined (Exercise 33). µSmalltalk’s
predefined numbers are more interesting: objects of class Integer represent inte‐
gers, and instances of Fraction and Float represent rational numbers. A Fraction
represents a rational number as the ratio of numerator n to denominator d. A Float
represents a rational number of the form m · 10e , where m is an integer mantissa
and e is an integer exponent.
These predefined numeric classes support all of the arithmetic and comparison
operations in the Magnitude and Number protocols, but they support only homoge
neous operations: for example, integers can be compared only with integers, frac‐
tions only with fractions, and floating‐point numbers only with floating‐point num‐
bers. The same goes for addition, subtraction, multiplication, and so on. For ex‐
ample, the fraction 3/4 can’t be asked if it is less than the integer 1—and yet, such
questions are both convenient and in the spirit of object‐oriented programming.
Fortunately, mixed comparisons and arithmetic are not hard to implement (Exer‐
cise 36).
Integers Integers provide not just the standard arithmetic operations expected of
all numbers, but also div:, mod:, gcd:, and lcm:, which are defined only on inte‐
gers. And an integer n can be told to evaluate a block n times. In µSmalltalk, in‐
tegers come in two forms: small integers, which I implement, and large integers,
which I invite you to implement (Exercise 38). The integer subhierarchy looks like
this:
Integer
SmallInteger LargeInteger
LargePositiveInteger LargeNegativeInteger
These classes are discussed in Section 10.8.3 (page 667), and they all answer the
protocol shown in Figure 10.21.
Fractions A Fraction represents a rational number as a fraction. For example,
17
in the session below, sending (two sqrtWithin: epsilon) answers 12 , which ap‐
√ 1
proximates 2 to within epsilon ( 10 ). The approximation, 1.416̄, is quite close to
the actual value of 1.4142+.
10 652a. htranscript 611ai+≡
‑> (val two (Fraction num:den: 2 1))
◁ 648 652b ▷
652 Better precision can be had by decreasing epsilon. And fractions can be com‐
puted conveniently and idiomatically by dividing integers or by using asFraction.
652b. htranscript 611ai+≡ ◁ 652a 652c ▷
‑> (val epsilon (1 / 100))
1/100
‑> (val root2 ((2 asFraction) sqrtWithin: epsilon))
577/408
The square root of n is computed by using the Newton‐Raphson technique for find‐
ing a zero of the function x2 − n. Newton‐Raphson produces accurate results
quickly; the approximation 577
408 is accurate to five decimal places.
Floatingpoint numbers A number in floating‐point form is represented by a man‐
tissa m and exponent e, which stand for the number m · 10e . Both m and e can
be negative. A Float satisfies the representation invariant that |m| ≤ 32767; this
restriction, which provides about 15 bits of precision, ensures that two mantissas
can be multiplied without arithmetic overflow.
Class Float can be used as follows:
652c. htranscript 611ai+≡ ◁ 652b
‑> (val epsilon ((1 / 100) asFloat))
1x10^‑2
‑> ((2 asFloat) sqrtWithin: epsilon)
14142x10^‑4
A natural number is more than a magnitude but less than a full Number: µSmalltalk’s
class Natural supports limited arithmetic and a couple of new observers. A natu‐
ral number is a form of magnitude, and class Natural is a subclass of Magnitude.
In addition to the protocol for Magnitude, including comparisons, class Natural
and its instances respond to the protocol in Figure 10.22 on the next page.
Natural numbers may be added and multiplied without fear of overflow—the
size of a natural number is limited only by the amount of memory available. Natu‐
ral numbers may also be subtracted, provided that the argument is no greater than
the receiver. And a natural number may be divided by a small, positive integer;
the s in sdiv: and smod: stands for “short.” (As noted on page S19, long division is
beyond the scope of this book.)
The protocol for instances of Natural includes an observer decimal, which con‐
verts the receiver to a list of decimal digits. (For efficiency, the receiver is expected
to use an internal representation with a base much larger than 10.)
fromSmall: anInteger Answer a natural‐number object whose value is
equal to the value of the argument, which must be a
nonnegative integer.
(a) Class protocol for Natural
Finally, the Natural protocol includes three methods that are intended to pro‐
mote efficiency:
The implementation of class Natural is left to you (Exercise 37). Ideas are pre‐
sented on page 669.
10.5 OBȷECT‐ORıENTED PROGRAMMıNG TECHNıQUEſ
To get started programming in µSmalltalk, you can focus on the examples from Sec‐
tion 10.1 and on the protocols and informal descriptions of the predefined classes in
10 Section 10.4. But to internalize object‐oriented ways of thinking and programming,
you need to study more deeply. Four increasingly deep techniques are presented
in the next four sections:
Smalltalk and • Section 10.6 shows how to make decisions by dispatching messages to the
objectorientation right methods, not by evaluating conditionals. The technique is illustrated
with example methods defined on classes Object, UndefinedObject, and
654
Boolean.
• Section 10.7 shows how to reuse code by building abstract classes that pro‐
vide many useful methods on top of just a few subclass responsibilities.
The technique is illustrated with example methods defined on collection
classes.
• Section 10.8 shows how to define methods that want to look at representa‐
tions of more than one object, even though a method defined on an object
has access only to its own instance variables. The technique is illustrated
with example methods defined on numeric classes.
• Section 10.9 shows how to integrate ideas from Sections 10.6 and 10.7 with
program‐design ideas from Chapter 9, including an abstraction function and
representation invariant. The techniques are illustrated with a complete im‐
plementation of class List.
This code makes a real Smalltalk programmer cringe. In Smalltalk, case analysis
should be implemented by method dispatch. For isNil there are only two possi‐
ble cases: an object is nil or it isn’t. I arrange that on the nil object, the isNil
method answers true, and that on all other objects, it answers false. I need only
two method definitions: one on class UndefinedObject, which is used only to an‐
swer messages sent to nil, and one on class Object, which all other classes inherit.
I implement notNil the same way. The definitions on class UndefinedObject are
654. hmethods of class UndefinedObject 654i≡ S559d ▷
(method isNil () true) ;; definitions on UndefinedObject
(method notNil () false)
On class Object, they are
655a. hmethods of class Object 634i+≡ ◁ 635a S558c ▷
(method isNil () false) ;; definitions on Object
(method notNil () true)
The definitions contain no conditionals; decisions are made entirely by method §10.6
dispatch. For example, when isNil is sent to nil, nil is an instance of class Technique I:
UndefinedObject, so the message dispatches to UndefinedObject’s isNil method, Method dispatch
which answers true. But when isNil is sent to any other object, method search replaces
starts in the class of that object and eventually reaches class Object, where it dis‐ conditionals
patches to Object’s isNil method, which answers false. A notNil message works
the same way. 655
If you take object‐oriented programming seriously, you will never use an explicit
conditional if you can achieve the same effect using method dispatch. Method dispatch
is preferred because it is extensible: to add new cases, you just add new classes—
think, for example, about adding new kinds of shapes to the pictures in Section 10.1.
To add new cases to a conditional, you would have to edit the code—at every location
where a similar conditional decision is made. Method dispatch makes it easier to
evolve the code. And in many implementations, it is also more efficient.
Some conditionals can’t be avoided. For example, to know if a number n is
at least 10, we must send a conditional message like ifTrue: to the Boolean ob‐
ject produced by (n >= 10). But ifTrue: is itself implemented using method dis‐
patch! Smalltalk’s conditionals and loops aren’t written using syntactic forms like
µScheme’s if and while, because Smalltalk has no such forms—it has only mes‐
sage passing and return. Conditionals are implemented by sending continuations
to Boolean objects, and loops are implemented by sending continuations to block
objects.10
A conditional method like ifTrue: is implemented in both of the subclasses of
Boolean: True and False. By the time a method is activated, the conditional deci‐
sion has already been made by method dispatch; the method has only to take the
action appropriate to the subclass it is defined on. Each subclass defines methods
according to what its instance represents; for example, class True has one instance,
true, which represents truth, and its methods are defined accordingly:
655b. hpredefined µSmalltalk classes and values 655bi≡ S548d ▷
(class True
[subclass‑of Boolean]
(method ifTrue: (trueBlock) (trueBlock value))
(method ifFalse: (falseBlock) nil)
(method ifTrue:ifFalse: (trueBlock falseBlock) (trueBlock value))
(method ifFalse:ifTrue: (falseBlock trueBlock) (trueBlock value))
The implementation of class False, which is similar, is left as Exercise 16. The in‐
genious division of class Boolean into subclasses True and False is owed to Dan
Ingalls (2020).
10
This implementation of conditionals is the same one used in the Church encoding of Booleans in the
lambda calculus—a tool used in the theoretical study of programming languages.
10.7 TECHNıQUE II: ABſTRACT CLAſſEſ
The Magnitude protocol suits any abstraction that is totally ordered, even those
that do not support arithmetic: numbers, dates, times, and so on. A subclass of
Magnitude has only two responsibilities: comparisons = and <.11
656a. hnumeric classes 656ai≡ (S548d) 664a ▷
(class Magnitude
[subclass‑of Object] ; abstract class
(method = (x) (self subclassResponsibility)) ; may not inherit
(method < (x) (self subclassResponsibility))
hother methods of class Magnitude 656bi
)
The other comparisons, as well as min: and max:, are implemented using <, and
they can be reused in every subclass.
656b. hother methods of class Magnitude 656bi≡ (656a)
(method > (y) (y < self))
(method <= (x) ((self > x) not))
(method >= (x) ((self < x) not))
(method min: (aMagnitude)
((self < aMagnitude) ifTrue:ifFalse: {self} {aMagnitude}))
(method max: (aMagnitude)
((self > aMagnitude) ifTrue:ifFalse: {self} {aMagnitude}))
Another big protocol that relies on just a few subclass responsibilities is the
Collection protocol. This protocol is a joy to work with; it includes not only object‐
oriented analogs to functions like exists?, map, filter, and foldl, but also many
other methods, which do things like add, remove, find, and count elements (Fig‐
ure 10.15, page 644). And unlike their Scheme analogs, these operations support
not only lists but also several other forms of collection. All this functionality is
provided using just four subclass responsibilities: a collection class must define
methods do:, add:, remove:ifAbsent:, and =.
656c. hcollection classes 656ci≡ (S548d) 659a ▷
(class Collection
[subclass‑of Object] ; abstract
(method do: (aBlock) (self subclassResponsibility))
(method add: (newObject) (self subclassResponsibility))
(method remove:ifAbsent: (oldObject exnBlock)
(self subclassResponsibility))
(method = (aCollection) (self subclassResponsibility))
hother methods of class Collection 657ai
)
To see how these subclass responsibilities are implemented, look at class Set (Ap‐
pendix U, page S562). To see how the subclass responsibilities are used, look below.
11
According to the rules of Smalltalk, a magnitude may not inherit the default implementation of =
from class Object, which is object identity. That’s why method = is redefined as a subclass responsibility.
To create a singleton collection, we send add: to a new instance; to create a col‐
lection holding all of an argument’s elements, we send addAll: to a new instance.
657a. hother methods of class Collection 657ai≡ (656c) 657b ▷
(class‑method with: (anObject)
((self new) add: anObject))
(class‑method withAll: (aCollection)
((self new) addAll: aCollection))
§10.7
When addAll: is sent to an object of a subclass, the message dispatches to the Technique II:
method shown here, which is defined on class Collection. It is implemented using Abstract classes
do: and add:.
657b. hother methods of class Collection 657ai+≡ (656c) ◁ 657a 657c ▷
657
(method addAll: (aCollection)
(aCollection do: [block (x) (self add: x)])
self)
When method addAll: sends do: and add:, they dispatch to the methods defined
on the subclass.
Removal works in the same way, building on do: and remove:ifAbsent:.
657c. hother methods of class Collection 657ai+≡ (656c) ◁ 657b 657d ▷
(method remove: (oldObject)
(self remove:ifAbsent: oldObject {(self error: 'remove‑was‑absent)}))
(method removeAll: (aCollection)
(aCollection do: [block (x) (self remove: x)])
self)
Using a linear search to compute size, for example, may seem inefficient, but if
a subclass knows a more efficient way to compute the number of elements, it re‐
defines the size method. And for some collections, like List, there is no more
efficient way to compute size or count occurrences.
An iteration that uses do: can be cut short by a return expression, as in meth‐
ods isEmpty, includes:, and detect:ifNone: below. And again, if the collection
is a linked list, no more efficient implementation is possible.
657e. hother methods of class Collection 657ai+≡ (656c) ◁ 657d 658a ▷
(method isEmpty ()
(self do: [block (_) (return false)])
true)
(method includes: (anObject)
(self do: [block (x) ((x = anObject) ifTrue: {(return true)})])
false)
(method detect:ifNone: (aBlock exnBlock)
(self do: [block (x) ((aBlock value: x) ifTrue: {(return x)})])
(exnBlock value))
(method detect: (aBlock)
(self detect:ifNone: aBlock {(self error: 'no‑object‑detected)}))
species Answer a class that should be used to create new instances of
collections like the receiver, to help with the implementation of
select:, collect:, and similar methods.
10 printName Print the name of the object’s class, to help with the
implementation of print. (Almost all Collection objects print
as the name of the class, followed by the list of elements in
parentheses. Array objects omit the name of the class.)
Smalltalk and
objectorientation Figure 10.23: Private methods internal to Collection classes.
658
In addition to mutators and observers, the Collection protocol also pro‐
vides iterators. These iterators are akin to µScheme’s higher‐order functions on
lists. For example, do: resembles µScheme’s app, and inject:into: resembles
µScheme’s foldl. But as before, objects offer this advantage: unlike µScheme’s
foldl, inject:into: works on any collection, not just on lists.
658a. hother methods of class Collection 657ai+≡ (656c) ◁ 657e 658b ▷
(method inject:into: (aValue binaryBlock)
(self do: [block (x) (set aValue (binaryBlock value:value: x aValue))])
aValue)
The methods select:, reject:, and collect: resemble µScheme’s filter and
map functions. Like inject:into:, they work on all collections, not just on lists.
The implementations use species, which is a private message used to create “a new
collection like the receiver” (Figure 10.23).
658b. hother methods of class Collection 657ai+≡ (656c) ◁ 658a 658c ▷
(method select: (aBlock) [locals temp]
(set temp ((self species) new))
(self do: [block (x) ((aBlock value: x) ifTrue: {(temp add: x)})])
temp)
(method reject: (aBlock)
(self select: [block (x) ((aBlock value: x) not)]))
(method collect: (aBlock) [locals temp]
(set temp ((self species) new))
(self do: [block (x) (temp add: (aBlock value: x))])
temp)
remove:ifAbsent associationsDo:,
removeKey:ifAbsent:
§10.7
associationsDo: add, at:put, firstKey, lastKey at:IfAbsent:
Seq.
Technique II:
remove:ifAbsent,
Abstract classes
removeKey:ifAbsent,
species 659
Collection isn’t just an abstract class with multiple implementations. It’s an ab‐
straction that is refined into more abstractions:
• Keyed collections, which collect key‐value pairs and can be indexed by key
• Sequenceable collections, which are keyed by consecutive integers
Each of these collections refines the protocol defined by its superclass. To study
such refinement, we ask the same questions about each subclass:
For keyed and sequenceable collections, the answers are shown in Table 10.24.
Implementation of KeyedCollection
Because keys are consecutive integers, method at:ifAbsent: can track the value
of the key inside a do: loop, without ever allocating an Association. This imple‐
mentation is more efficient than the generic implementation inherited from class
KeyedCollection.
Method associationsDo: also loops over consecutive keys.
661b. hother methods of class SequenceableCollection 661bi≡ (661a)
(method associationsDo: (bodyBlock) [locals i last]
(set i (self firstKey))
(set last (self lastKey))
({(i <= last)} whileTrue:
{(bodyBlock value: (Association withKey:value: i (self at: i)))
(set i (i + 1))}))
In a language that uses abstract data types, like Molecule, complex operations like
these are easy to implement: if an operation can see the definition of a type, it can
see the representation of every argument of that type. But in a pure object‐oriented
language, like Smalltalk, a complex method isn’t so easy to implement: it can see
only the representation of the receiver, and all it can do with an argument is send
messages to it. To figure out what messages to send, we have several options:
• We can extend the argument’s protocol with new messages that provide ac‐
cess to its representation. This technique is illustrated using classes Number,
Integer, and especially Fraction (Sections 10.8.1 and 10.8.2).
• We can coerce one object to have the same representation as another. For ex‐
ample, to add aNumber to a fraction, we can coerce aNumber to a fraction.
This technique is illustrated using the Fraction and Integer classes (Sec‐
tion 10.8.4).
All three techniques can work with any representation. To help you integrate them
into your own programming, I recommend a case study for which there is more
than one reasonable representation: arithmetic on natural numbers. A natural §10.8
number should be represented as a sequence of digits, and every representation of Technique III:
that sequence suggests its own set of new messages that are analogous to the new Multiple
messages defined on class Fraction (Section 10.8.5). representations the
objectoriented way
10.8.1 A context for complex operations: Abstract classes Number and Integer 663
;;;;;;; coercion
(method asInteger () (self subclassResponsibility))
(method asFraction () (self subclassResponsibility))
(method asFloat () (self subclassResponsibility))
(method coerce: (aNumber) (self subclassResponsibility))
hother methods of class Number 663bi
)
The Number protocol also requires methods squared, sqrt, sqrtWithin:, and
raisedToInteger:, which are relegated to the Supplement.
Before we get to class Fraction, we should also sketch class Integer, which
provides the gcd: operation needed to put a fraction in lowest terms. The gcd:
method is one of the four division‐related methods gcd:, lcm:, div:, and mod:.
Only div: has to be implemented in subclasses.
10 664a. hnumeric classes 656ai+≡
(class Integer
(S548d) ◁ 656a 664c ▷
Class Integer typically has three concrete subclasses: SmallInteger, for inte‐
gers that fit in a machine word (Appendix U); LargePositiveInteger, for arbitrar‐
ily large positive integers; and LargeNegativeInteger, for arbitrarily large neg‐
ative integers (Section 10.8.3). In µSmalltalk, only SmallInteger is defined; the
other two are meant to be added by you (Exercise 38).
Class Integer implements the subclass responsibility reciprocal, and it also
overrides the default / method. Both methods answer fractions, not integers.
664b. hother methods of class Integer 664bi≡ (664a) 669b ▷
(method reciprocal () (Fraction num:den: 1 self))
(method / (aNumber) ((self asFraction) / aNumber))
Methods num and den are used to implement the four complex operations =, <,
*, and +. Each operation relies on and guarantees these representation invariants:
These invariants imply that two Fraction objects represent the same fraction if
and only if they have the same numerator and denominator, and they enable the
following implementations of the comparison methods from class Magnitude: §10.8
Technique III:
665a. hother methods of class Fraction 664di+≡ (664c) ◁ 664d 665b ▷
(method = (f) ((num = (f num)) and: {(den = (f den))}))
Multiple
(method < (f) ((num * (f den)) < ((f num) * den))) representations the
′
′ ′ objectoriented way
The < method uses the law that n d < d′ if and only if n · d < n · d, which holds
n
′
only because d and d are positive. And argument f doesn’t have to be an instance of 665
class Fraction; it’s enough if f is a number and if it responds sensibly to messages
num and den.
Methods = and < rely on the representation invariants. The invariants for any
given fraction are established by two private methods: method signReduce estab‐
lishes invariant 1, and method divReduce establishes invariants 2 and 3.
665b. hother methods of class Fraction 664di+≡ (664c) ◁ 665a 665c ▷
(method signReduce () ; private
((den isZero) ifTrue: {(self error: 'ZeroDivide)})
((den isNegative) ifTrue:
{(set num (num negated)) (set den (den negated))})
self)
(method divReduce () [locals temp] ; private
((num = 0) ifTrue:ifFalse:
{(set den 1)}
{(set temp ((num abs) gcd: den))
(set num (num div: temp))
(set den (den div: temp))})
self)
When a new Fraction is created by public class method num:den:, all three invari‐
ants are established by private method initNum:den:.
665c. hother methods of class Fraction 664di+≡ (664c) ◁ 665b 665d ▷
(class‑method num:den: (a b) ((self new) initNum:den: a b))
(method setNum:den: (a b) (set num a) (set den b) self) ; private
(method initNum:den: (a b) ; private
(self setNum:den: a b)
(self signReduce)
(self divReduce))
666 Method + is the last of the complex methods. But it’s not the last of the methods
that rely on or guarantee invariants. For example, reciprocal must not leave a
negative fraction with a negative denominator. The denominator is given the cor‐
rect sign by sending signReduce to the inverted fraction. (The reciprocal of zero
cannot be put into reduced form. Nothing can be done about it.)
666b. hother methods of class Fraction 664di+≡ (664c) ◁ 666a 666c ▷
(method reciprocal ()
(((Fraction new) setNum:den: den num) signReduce))
The invariants enable the sign tests to inspect only the receiver’s numerator.
These tests are much more efficient than the versions inherited from Number.
666d. hother methods of class Fraction 664di+≡ (664c) ◁ 666c 668a ▷
(method isZero () (num isZero))
(method isNegative () (num isNegative))
(method isNonnegative () (num isNonnegative))
(method isStrictlyPositive () (num isStrictlyPositive))
Given methods num and den, fractions can be compared with fractions, added to
fractions, and so on. Comparison and arithmetic methods can’t see the represen‐
tations of their arguments, but they don’t have to: it’s enough to know that each
argument responds to messages num and den. But comparison and arithmetic on
integers are not so easy.
The difficulty with integers is that Smalltalk supports two forms of integer:
small and large. A small integer must fit in a machine word; a large integer may be
arbitrarily large. Both forms must respond to a + message, but the algorithm used
to implement + depends on what is being added:
• If two small integers are being added, the algorithm uses primitive addition,
which ultimately executes a hardware addition instruction.
• If two large integers are being added, the algorithm adds the digits pairwise,
with carry digits, as described in Appendix B.
• If a large integer and a small integer are being added, the algorithm coerces
the small integer to a large integer, then adds the resulting large integers.
The appropriate algorithm depends on more than just the class of the receiver.
To select the appropriate algorithm, the + method doesn’t interrogate objects
to ask about their classes. (The object‐oriented motto is, “Don’t ask; tell.”) Instead,
when a small integer receives a + message, its + method sends another message to
its argument, saying, “I’m a small integer; add me to yourself.”
667. hSmallInteger methods revised to use double dispatch 667i≡ §10.8
(method + (anInteger) (anInteger addSmallIntegerTo: self))
Technique III:
The addSmallIntegerTo: method knows that its argument is a small integer, and Multiple
like all methods, it knows what class it’s defined on. This is enough information to representations the
choose the right algorithm: objectoriented way
The rest of Figure 10.25 describes the other methods needed to implement
arithmetic on large integers. Method magnitude plays the same role for large inte‐
gers that methods num and den play for fractions. And methods sdiv: and smod:
provide a protocol for dividing a large integer by a small integer. (Division of a large
integer by another large integer requires long division. Long division is fascinating
algorithmically, but it’s too hairy to make a good exercise.)
A starter kit for class LargeInteger is shown in Figure 10.26 (page 669). The
LargeInteger class is meant to be abstract; do not instantiate it. Instead, de‐
fine subclasses LargePositiveInteger and LargeNegativeInteger, which you
can then instantiate (Exercise 38).
A binary message like < or + should be sent only when the receiver and the argu‐
ment are compatible. If numbers aren’t compatible, they can be made so using co
ercion. In Smalltalk, coercion is part of the Number protocol; every number must be
able to coerce itself to an integer, a floating‐point number, or a fraction. A coercion
addSmallIntegerTo: aSmallInteger
Answer the sum of the argument and the receiver.
addLargePositiveIntegerTo: aLargePositiveInteger
fromSmall: aSmallInteger
Answer a large integer whose value is equal to the
value of the argument.
withMagnitude: aNatural
Answer an instance of the receiver whose magnitude
is the given magnitude.
(b) Class protocol for LargeInteger
method typically uses the public protocol of the classes it is coercing its receiver to,
like these methods defined on class Fraction:
668a. hother methods of class Fraction 664di+≡ (664c) ◁ 666d 668b ▷
(method asInteger () (num div: den))
(method asFloat () ((num asFloat) / (den asFloat)))
(method asFraction () self)
The coercion methods on classes Float and Integer follow the same structure.
Class Float is relegated to Appendix U, but the Integer methods are shown here.
Just as a fraction must know what integer or floating‐point operations to use to di‐
vide its numerator by its denominator, an integer must know what fractional or
floating‐point operations to use to represent an integer. In this case, it’s self di‐
vided by 1 and self times a base to the power 0, respectively.
669b. hother methods of class Integer 664bi+≡ (664a) ◁ 664b 669c ▷
(method asFraction () (Fraction num:den: self 1))
(method asFloat () (Float mant:exp: self 0))
Just as in class Fraction, the other two methods simply exploit the knowledge that
the receiver is an integer:
669c. hother methods of class Integer 664bi+≡ (664a) ◁ 669b S567a ▷
(method asInteger () self)
(method coerce: (aNumber) (aNumber asInteger))
Using representations that I have defined, the examples above demonstrate tech‐
niques used to implement complex methods like + and <. The same techniques
can be applied to a representation that you can define: a representation of natural
numbers (Exercise 37). To get started, follow the guidance below, which presents
hints, ideas, and private protocols for two possible representations.
670. hnumeric classes 656ai+≡ (S548d) ◁ 664c S567b ▷
(class Natural
[subclass‑of Magnitude]
; instance variables left as an exercise
X
degree
X= xi · bi .
i=0
With this array representation, I recommend the private protocol shown in Fig‐
ure 10.28 on the next page.
base Answers b.
(a) Private class method for class Natural
Figure 10.28: Suggested private methods for class Natural, array representation
• The base method on the class provides a single point of truth about b, which
you choose.
• The digit‐related methods are used to read, write, and iterate over digits.
• Methods trim and degree are used to keep the arrays as small as possible, so
leading zeroes don’t accumulate.
The array representation offers these trade‐offs: Because it provides easy ac‐
cess to any digit you like, it enables you to treat Smalltalk as if it were a procedural
language, like C. In particular, you can get away without thinking too hard about
dynamic dispatch, because a lot of decisions can be made by looking at digits and
at degree. But the individual methods are a little complicated, and you may not
learn a whole lot—my array‐based code uses objects only to hide information from
client code, and it doesn’t exploit dynamic dispatch or inheritance.
Figure 10.29: Suggested private methods for class Natural, subclass representation
• Class method first:rest: creates a new instance of one of the two sub‐
classes. If both arguments are zero, it answers an instance of class NatZero.
Otherwise, it answers an instance of class NatNonzero.
sentinel 1 2 3
The sentinel contains two pointers: the cdr (solid line), which it inherits from class
Cons, points to the elements of the list, if any; the pred (dashed line), which is found
only on objects of class ListSentinel, points to the sentinel’s predecessor.
A sentinel’s predecessor is normally the last element of its list, but when a list
is empty, both fields of its sentinel point to the sentinel itself:
sentinel
When the cdr points to the sentinel itself, the abstraction function maps the rep‐
resentation to the empty sequence. When the cdr points to another object, that
object is a cons cell, and the abstraction function maps the representation to the
sequence of objects stored in the cons cells pointed to by the cdr of the sentinel.
Each cons cell, including the sentinel, responds to the protocol shown in Fig‐
ure 10.30 (on the next page).
10 A List object has only one instance variable: a pointer to the sentinel.
674a. hcollection classes 656ci+≡ (S548d) ◁ 661a S563 ▷
hclasses that define cons cells and sentinels 675bi
(class List
Smalltalk and
[subclass‑of SequenceableCollection]
objectorientation
[ivars sentinel]
(class‑method new () ((super new) sentinel: (ListSentinel new)))
674
(method sentinel: (s) (set sentinel s) self) ; private
(method isEmpty () (sentinel == (sentinel cdr)))
(method last () ((sentinel pred) car))
(method do: (aBlock) ((sentinel cdr) do: aBlock))
hother methods of class List 674bi
)
The method addLast: mutates a list by adding an element to the end. This
means inserting an element just after the predecessor of the sentinel. Similarly,
addFirst: inserts an element just after the sentinel. Having a sentinel means there
is no special‐case code for an empty list.
674b. hother methods of class List 674bi≡ (674a) 674c ▷
(method addLast: (item) ((sentinel pred) insertAfter: item) self)
(method addFirst: (item) (sentinel insertAfter: item) self)
(method add: (item) (self addLast: item))
Method removeFirst removes the element after the sentinel; removeLast is left
as Exercise 25.
674c. hother methods of class List 674bi+≡ (674a) ◁ 674b 674d ▷
(method removeFirst () (sentinel deleteAfter))
(method removeLast () (self leftAsExercise))
List element n is updated by skipping n cons cells and then sending the next cons
call the car: message.
675a. hother methods of class List 674bi+≡ (674a) ◁ 674f
(method at:put: (n value) [locals tmp]
(set tmp (sentinel cdr))
({(n isZero)} whileFalse:
{(set n (n ‑ 1))
(set tmp (tmp cdr))})
(tmp car: value)
self)
If n is out of range, the method can produce wrong answers—which can be made
right (Exercise 26).
The low‐level work of manipulating pointers is done by the methods in the cons‐
cell protocol (Figure 10.30).
675b. hclasses that define cons cells and sentinels 675bi≡ (674a) 677 ▷
(class Cons
[subclass‑of Object]
[ivars car cdr]
hmethods of class Cons 676ai
)
The first four methods of class Cons expose the representation as a pair of car
and cdr. And the pred: method makes it possible to tell any cons cell what its pre‐
decessor is—information that is used only by the sentinel. (A sentinel is an instance
of a subclass of Cons.)
10 676a. hmethods of class Cons 676ai≡
(method car () car)
(675b) 676b ▷
676 Methods deleteAfter and insertAfter: implement the standard pointer ma‐
nipulations for a circularly linked list. Circularity comes into play when a node is
deleted or inserted; sending pred: notifies the node’s successor of its new prede‐
cessor.
676b. hmethods of class Cons 676ai+≡ (675b) ◁ 676a 676c ▷
(method deleteAfter () [locals answer]
(set answer (cdr car))
(set cdr (cdr cdr))
(cdr pred: self)
answer)
(method insertAfter: (anObject)
(set cdr (((Cons new) cdr: cdr) car: anObject))
((cdr cdr) pred: cdr)
anObject)
The iteration and removal methods take full advantage of object‐oriented pro‐
gramming. By defining do: differently on classes and ListSentinel, I create
code that iterates over a list without ever using an explicit if or while—all it does
is method dispatch. To make the computation a little clearer, I present some
of the methods of class Cons right next to the corresponding methods of class
ListSentinel.
The do: method iterates over a list of cons cells by first doing the car, then
continuing with a tail call to the do: method of the cdr. The iteration terminates in
the sentinel, whose do: method does nothing.
676c. hmethods of class Cons 676ai+≡ (675b) ◁ 676b 676e ▷
(method do: (aBlock) ; defined on an ordinary cons cell
(aBlock value: car)
(cdr do: aBlock))
• Every value is an object, and every object has both a class and a representation.
When necessary, a value v is written in the form v = h|class, rep |i.
• The behaviors of literal integers, symbols, and arrays are defined by classes,
and if class SmallInteger, Symbol, or Array is redefined, the behaviors of
the associated literals change. For example, if you complete Exercises 36, 38,
and 39, you will change the behavior of integer literals to provide a seamless,
Table 10.31: Components of an initial state
10 e
ρ
Expression
Environment
Expression being evaluated
Instance variables, arguments, and local variables
csuper Class Destination for messages to super
F Stack frame Method activation that is terminated by return
Smalltalk and ξ Environment Global variables
objectorientation σ Store Current values of all variables
F Frame set Every activation of every method ever
678
• Environment ξ holds the locations of the global variables. It’s needed be‐
cause unlike a µScheme function, a method is given access to the global vari‐
ables defined at the time it receives a message, not at the time it is defined.
• Class csuper tracks the static superclass of the method definition within which
an expression is evaluated. This class, which is not the same as the super‐
class of the object that received the message (Exercise 9), is the class at which
method search begins when a message is sent to super. A static superclass
is associated with every method and every block, and for all the expressions
and blocks of a single method, it remains unchanged.
• Stack frame F tracks the method activation that return should cause to
return. Like a static superclass, an active frame is associated with every
method and every block, and for all the expressions and blocks of a single
activation of a single method, it remains unchanged.
• Set F records all the stack frames that have ever been used. It is used to
ensure that every time a method is activated, the activation is uniquely iden‐
tified with a new stack frame F . That device ensures in turn that if a return
escapes its original activation, any attempt to evaluate that return results in
a checked run‐time error. The frame set F is threaded throughout the entire
computation, in much the same way as the store σ .
Each individual component is used in a straightforward way, but the sheer num‐
ber can be intimidating. No wonder most theorists prefer to work with functional
languages!
The seven components listed in Table 10.31 form the initial state of an abstract
machine for evaluating µSmalltalk: he, ρ, csuper , F, ξ, σ, Fi. If expression e is eval‐
uated successfully, the machine transitions, expressing one of two behaviors:
• If an expression terminates normally and produces a value v , this behavior is
represented by a judgment of the form he, ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i .
As usual for a language with imperative features, evaluating e can change
the values of variables, so evaluation results in a new store σ ′ . And evalu‐
ation may send messages and allocate new stack frames, so evaluation also
produces a new used‐frame set F ′ . But the main result of evaluation is the
value v ; σ ′ and F ′ just capture effects. To make the judgment a little easier to §10.10
read, v is separated from the effects using a semicolon, not a comma. Operational
semantics
• If an expression evaluates a return, it immediately terminates an activa‐
tion of the method in which the return appears. I say it “returns v to 679
frame F ′ ,” and the behavior is represented by a judgment of the form
he, ρ, csuper , F, ξ, σ, Fi ↑ hv, F ′ ; σ ′ , F ′ i , with an arrow pointing up. Again,
the main results are separated from the effects by a semicolon.
If a syntactic form contains an expression, its evaluation can end in return behav‐
ior. But while we are learning the main part of the language, return behaviors are
distracting. For that reason, the semantics of return are presented in their own
section.
Another complication of return is that its evaluation can terminate the eval‐
uation of a list of expressions. To express this behavior precisely, the semantics
uses new judgment forms that describe the possible outcomes of evaluating a list
of expressions.
(EMPTYLıſT)
h[ ], ρ, csuper , F, ξ, σ, Fi ⇓ h[ ]; σ, Fi
The part of µSmalltalk’s semantics that is most compatible with µScheme is the part
that applies to situations in which no return is evaluated. Except for return, each
10 syntactic form in Figure 10.9 (page 628) has a rule that describes its non‐returning
evaluation. (The semantics of return is deferred to Section 10.10.2.)
Variables and assignment As in Impcore, environments ρ and ξ track local and
Smalltalk and global variables, but as in µScheme, they bind each defined name x to a muta‐
objectorientation ble location. Aside from the extra bookkeeping imposed by messages to super and
by returns, which manifests as extra components in the abstract‐machine state,
680 nothing here is new.
x ∈ dom ρ ρ(x) = ℓ
(VAR)
hVAR(x), ρ, csuper , F, ξ, σ, Fi ⇓ hσ(ℓ); σ, Fi
x∈ / dom ρ x ∈ dom ξ ξ(x) = ℓ
(GLOBALVAR)
hVAR(x), ρ, csuper , F, ξ, σ, Fi ⇓ hσ(ℓ); σ, Fi
Assignment to x translates x into a location ℓ, then changes the value in ℓ. As in
µScheme, the store is threaded. The set of allocated stack frames is threaded in the
same way; evaluating expression e transitions that set from F to F ′ .
x ∈ dom ρ ρ(x) = ℓ he, ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i
(AſſıGN)
hſET(x, e), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ {ℓ 7→ v}, F ′ i
x∈
/ dom ρ x ∈ dom ξ ξ(x) = ℓ he, ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i
hſET(x, e), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ {ℓ 7→ v}, F ′ i
(AſſıGNGLOBAL)
Assignment to names self, super, true, false, and nil is not permitted, but this
restriction is enforced in the parser, so it need not be mentioned here.
Self and super In µSmalltalk’s syntax, self is treated as an ordinary variable, but
super is a distinct syntactic form. In most contexts, super is evaluated like self.
(VALUE)
hVALUE(v), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ, Fi
Literals A literal evaluates to an instance of SmallInteger, Symbol, or Array. Only
integer and symbol literals are formalized here.
(EMPTYBEGıN)
hBEGıN(), ρ, csuper , F, ξ, σ, Fi ⇓ hnil ; σ, Fi
e 6= ſUPER
he, ρ, csuper , F, ξ, σ, Fi ⇓ hh|c, r|i; σ0 , F0 i
h[e1 , . . . , en ], ρ, csuper , F, ξ, σ0 , F0 i ⇓ h[v1 , . . . , vn ]; σn , Fn i
m c @ METHOD(_, hx1 , . . . , xn i, hy1 , . . . , yk i, em , s)
F̂ ∈/ Fn
ℓ1 , . . . , ℓn ∈ / dom σn ℓ′ 1 , . . . , ℓ′ k ∈
/ dom σn
ℓ1 , . . . , ℓn , ℓ 1 , . . . , ℓ′ k all distinct
′
ρi = instanceVars(h|c, r|i)
ρa = {x1 7→ ℓ1 , . . . , xn 7→ ℓn }
ρl = {y1 7→ ℓ′1 , . . . , yk 7→ ℓ′k }
σ̂ = σn {ℓ1 7→ v1 , . . . ℓn 7→ vn , ℓ′1 7→ nil , . . . ℓ′k 7→ nil }
hem , ρi + ρa + ρl , s, F̂ , ξ, σ̂, Fn ∪ {F̂ }i ⇓ hv; σ ′ , F ′ i . (SEND)
hſEND(m, e, e1 , . . . , en ), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i
The premise on the first line shows that this is a rule for an ordinary send, not a
send to ſUPER. The rest of the rule has much in common with the closure rule for
µScheme:
• The premises on the next two lines show the evaluation of the receiver e and
of the arguments e1 , . . . , en . After these evaluations, we know we are send‐
ing message m to receiver r of class c with actual parameters v1 , . . . , vn , and
the store is σn .
• The equations for ρi , ρa , and ρl create environments for the receiver’s in‐
10 stance variables, the method’s formal parameters, and the method’s local
variables, respectively.
• The equation for σ̂ initializes the formal parameters and the local variables.
Smalltalk and
• Finally, the last premise shows the evaluation of the body of the method em ,
objectorientation
in the new environment created by combining environments for instance
682 variables, actual parameters, and local variables. Any returns go to the new
stack frame F̂ .
Each primitive p is described by its own rule. The most interesting one is the
value primitive, which evaluates a block. Its rule resembles the rule for send‐
ing a message, except unlike a method, a block has no local variables or instance
variables of its own. And the body of a block is evaluated using its stored return
h[e1 , . . . , en ], ρ, csuper , F, ξ, σ, Fi ↑ hv, F ′ ; σ ′ , F ′ i
Figure 10.32: Rules for propagation of returns (other RETURN rules appear in the
text)
Most of the rules for return describe variations on one situation: during the eval‐
uation of an expression e, one of e’s subexpressions returns, and this behavior
causes e also to return (Figure 10.32). But eventually the return terminates an acti‐
vation frame of the method in which it appears. When a return to frame F̂ reaches
a method body executing in frame F̂ , the result of the return becomes the result of
the ſEND that activated the method. As with messages to super, the gray parts of
the rule are the same as in SEND, and the black parts are different.
If method body em tries to return somewhere else, to F ′ , the whole ſEND op‐
eration returns to F ′ .
x ∈ dom ξ ξ(x) = ℓ F̂ ∈
/F
he, {}, ξ0 (Object), F̂ , ξ, σ, {F̂ } ∪ Fi ⇓ hv; σ ′ , F ′ i
(DEFıNEOLDGLOBAL)
hVAL(x, e), ξ, σ, Fi → hξ, σ ′ {ℓ 7→ v}, F ′ i
§10.11
The interpreter
x∈
/ dom ξ ℓ∈
/ dom σ F̂ ∈
/F
685
he, {}, ξ0 (Object), F̂ , ξ, σ, {F̂ } ∪ Fi ⇓ hv; σ ′ , F ′ i
(DEFıNENEWGLOBAL)
hVAL(x, e), ξ, σ, Fi → hξ{x 7→ ℓ}, σ ′ {ℓ 7→ v}, F ′ i
hVAL(it, e), ξ, σ, Fi → hξ ′ , σ ′ , F ′ i
(EVALEXP)
hEXP(e), ξ, σ, Fi → hξ ′ , σ ′ , F ′ i
Block definition DEFıNE is syntactic sugar for creating a block.
Class definition The evaluation of a class definition is rather involved; the inter‐
preter creates an object that represents the class. The details are hidden in the
function newClassObject, which I don’t specify formally. To see how it works, con‐
sult the code in chunk 695a.
x ∈ dom ξ ξ(x) = ℓ
v = newClassObject(d, ξ, σ)
(DEFıNEOLDCLAſſ)
hCLAſſD(d), ξ, σ, Fi → hξ, σ{ℓ 7→ v}, Fi
The key parts of µSmalltalk’s interpreter involve the elements that make Smalltalk
unique: objects and classes. An object is represented in two parts: a class and an
internal representation (ML types class and rep). The class determines the object’s
response to messages, and the internal representation holds the object’s state. And type class 686c
although the Smalltalk word is “object,” the ML type of its representation is called type rep 686a
value, just like whatever thing an expression evaluates to in every other interpreter
in this book.
685. hdefinitions of value and method for µSmalltalk 685i≡ (S548a) 686d ▷
withtype value = class * rep
The rep part of an object exposes one of the biggest differences between
µSmalltalk and Smalltalk‐80. In Smalltalk‐80, every object owns a collection of
mutable locations, called “instance variables,” each of which can be filled either
with an ordinary object or with a sequence of bytes. But because µSmalltalk is
implemented in ML, raw locations and sequences of bytes are not useful represen‐
tations. In µSmalltalk, every object owns a single representation, which is defined
by ML datatype rep. That representation may be a collection of named, mutable
locations representing instance variables, or it may be any of half a dozen other
primitive representations.
686a. hdefinitions of exp, rep, and class for µSmalltalk 686ai≡ (S548a) 686c ▷
10 datatype rep
= USER of value ref env (* ordinary object *)
| ARRAY of value Array.array
| NUM of int
Smalltalk and | SYM of name
objectorientation | CLOSURE of name list * exp list * value ref env * class * frame
| CLASSREP of class
686
| METHODV of method (* compiled method *)
fun instanceVars (_, USER rep) = rep instanceVars : value ‑> value ref env
| instanceVars self = bind ("self", ref self, emptyEnv)
The class and value representations inform the representations of the ele‐
ments of the abstract‐machine state (Table 10.31, page 678).
The value and frame types are also used to represent behaviors.
Fields value and to hold v and F . And in the unhappy event that a block tries
to return after its frame has died, field unwound is used to print diagnostics.
Raising the Return exception, like any other exception, interrupts compu‐
tation in exactly the same way as the propagated returns described in Fig‐
ure 10.32 (page 683). That’s not a coincidence; both exceptions and returns
are language features that are designed to interrupt planned computations.
Of the two major syntactic categories, expressions and definitions, it’s the defini‐ type active_send
tion category whose forms most resemble forms found in other languages. A defi‐ S590c
nition may be one of our old friends VAL and EXP, a block definition (DEFINE), or a bind 305d
emptyEnv 305a
class definition (CLASSD). A class definition may include method definitions, which
type env 304
come in two flavors: instance methods and class methods. type exp 688a
687b. hdefinition of def for µSmalltalk 687bi≡ (S548a) type frame S590a
datatype def = VAL of name * exp type name 303
type value 685
| EXP of exp
| DEFINE of name * name list * exp
| CLASSD of { name : string
, super : string
, ivars : string list
, methods : method_def list
}
and method_flavor = IMETHOD (* instance method *)
| CMETHOD (* class method *)
withtype method_def = { flavor : method_flavor, name : name
, formals : name list, locals : name list, body : exp
}
The expression category also contains many forms that resemble forms found
in other languages, but the forms that relate to literals are unique to µSmalltalk.
The forms defined by VAR, SET, SEND, BEGIN, and BLOCK have analogs in µScheme,
µML, and Molecule. (Even though blocks have two forms in concrete syntax, both
10 with and without parameters, they have just one form in abstract syntax, with a
list of parameters that might be empty.) Forms defined by RETURN, PRIMITIVE, and
METHOD have no analogs in other interpreters, but they are also typical abstract syn‐
tax. And SUPER simply makes it easy to recognize super and give it the semantics it
Smalltalk and
should have. But literal values are handled differently than in other interpreters.
objectorientation
A literal must ultimately evaluate to an object, whose representation has type
688 value. That representation includes a class, but until the interpreter is boot‐
strapped, most classes aren’t yet defined (Section 10.11.6). For example, a literal
integer can’t evaluate to an object until class Integer is defined. So a LITERAL ex‐
pression holds only a rep; its class isn’t computed until it is evaluated.
The LITERAL form is complemented by a VALUE form. This form is used in‐
ternally, primarily as a way to turn an object into an expression that can receive
a SEND. For example, the interpreter sends println to a VALUE form at the end of a
read‐eval‐print loop, and it sends = to a VALUE form when testing a check‑expect.
688a. hdefinitions of exp, rep, and class for µSmalltalk 686ai+≡ (S548a) ◁ 686c
and exp = VAR of name
| SET of name * exp
| SEND of srcloc * exp * name * exp list
| BEGIN of exp list
| BLOCK of name list * exp list
| RETURN of exp
| PRIMITIVE of name * exp list
| METHOD of name list * name list * exp list
| SUPER
| LITERAL of rep
| VALUE of class * rep
The abstract syntax for SEND includes a field that is not explicit in the concrete syn‐
tax: srcloc is the source‐code location of the SEND, and it is used in diagnostic
messages.
Internal function ev handles all the syntactic forms, the most interesting of which
are RETURN and SEND.
Evaluating returns and sends A RETURN evaluates the expression to be returned,
then returns to frame F by raising the Return exception.
689a. hfunction ev, the evaluator proper 689ai≡ (688b) 689b ▷
fun ev (RETURN e) = raise Return { value = ev e, to = F, unwound = [] }
That Return exception is caught by the code that interprets message send.
The SEND code carries a lot of freight: it implements most of rules SEND, SEND‐
SUPER, RETURNTO, and RETURNPAſT, and it also supports diagnostic tracing. The
send and return rules all follow the same outline; for reference, that outline is for‐ §10.11
malized by the first, highlighted part of the SEND rule: The interpreter
e 6= ſUPER 689
he, ρ, csuper , F, ξ, σ, Fi ⇓ hh|c, r|i; σ0 , F0 i
h[e1 , . . . , en ], ρ, csuper , F, ξ, σ0 , F0 i ⇓ h[v1 , . . . , vn ]; σn , Fn i
m c @ METHOD(_, hx1 , . . . , xn i, hy1 , . . . , yk i, em , s)
F̂ ∈/ Fn
ℓ1 , . . . , ℓn ∈ / dom σn ℓ′ 1 , . . . , ℓ′ k ∈
/ dom σn
ℓ1 , . . . , ℓn , ℓ 1 , . . . , ℓ′ k all distinct
′
ρi = instanceVars(h|c, r|i)
ρa = {x1 7→ ℓ1 , . . . , xn 7→ ℓn }
ρl = {y1 7→ ℓ′1 , . . . , yk 7→ ℓ′k }
σ̂ = σn {ℓ1 7→ v1 , . . . ℓn 7→ vn , ℓ′1 7→ nil , . . . ℓ′k 7→ nil }
hem , ρi + ρa + ρl , s, F̂ , ξ, σ̂, Fn ∪ {F̂ }i ⇓ hv; σ ′ , F ′ i . (SEND)
hſEND(m, e, e1 , . . . , en ), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i
Each SEND computation begins in the same way: evaluate the receiver and the ar‐
guments using ev, then use the syntax of the receiver to identify the class on which
method search begins. Message send dispatches on the receiver, whose class is
used to find the method that defines message, except when the message is sent
to super, in which case the superclass of the currently running method is used.
At that point, because of tracing and returns, things start to get complicated, so let’s
look at the code, then focus on the anonymous function passed to trace:
689b. hfunction ev, the evaluator proper 689ai+≡ (688b) ◁ 689a 691a ▷
| ev (SEND (srcloc, receiver, msgname, args)) = type class 686c
let val obj as (class, rep) = ev receiver type env 304
val vs = map ev args findMethod 690b
val startingClass = type frame S590a
invokeMethod
case receiver of SUPER => superclass | _ => class
690a
hdefinition of function trace S583bi type name 303
in trace newFrame S590a
(fn () => type rep 686a
let val imp = findMethod (msgname, startingClass) Return 687a
val Fhat = newFrame () trace S583b
type value 685
in invokeMethod (imp, obj, vs, Fhat)
handle Return { value = v, to = F', unwound = unwound } =>
if F' = Fhat then
v
else
hreraise Return, adding msgname, class, and loc to unwound S590di
end)
end
The anonymous function calls findMethod to get imp from m c @ imp, and it
allocates F̂ as Fhat. It then delegates the second part of the SEND rules to function
invokeMethod, except for the conditions in the RETURNTO and RETURNPAſT rules.
Those conditions are dealt with by handle, which catches every Return exception.
If the Return is meant to terminate this very SEND—that is, if F ′ = F̂ —then the
anonymous function returns v as the result of the call to ev. If not, the anonymous
function re‐raises Return, adding information to the unwound list.
10 What about function trace? It wraps the action of the anonymous function,
so if findMethod results in a “message not understood” error, or if invokeMethod
results in some other run‐time error, trace can produce a stack trace. Function
trace is defined in Appendix U.
Smalltalk and
The second part of the SEND rule is implemented by function invokeMethod.
objectorientation
690 e 6= ſUPER
he, ρ, csuper , F, ξ, σ, Fi ⇓ hh|c, r|i; σ0 , F0 i
h[e1 , . . . , en ], ρ, csuper , F, ξ, σ0 , F0 i ⇓ h[v1 , . . . , vn ]; σn , Fn i
m c @ METHOD(_, hx1 , . . . , xn i, hy1 , . . . , yk i, em , s)
F̂ ∈/ Fn
ℓ1 , . . . , ℓn ∈ / dom σn ℓ′ 1 , . . . , ℓ′ k ∈
/ dom σn
ℓ1 , . . . , ℓn , ℓ 1 , . . . , ℓ′ k all distinct
′
ρi = instanceVars(h|c, r|i)
ρa = {x1 7→ ℓ1 , . . . , xn 7→ ℓn }
ρl = {y1 7→ ℓ′1 , . . . , yk 7→ ℓ′k }
σ̂ = σn {ℓ1 7→ v1 , . . . ℓn 7→ vn , ℓ′1 7→ nil , . . . ℓ′k 7→ nil }
hem , ρi + ρa + ρl , s, F̂ , ξ, σ̂, Fn ∪ {F̂ }i ⇓ hv; σ ′ , F ′ i
(SEND)
hſEND(m, e, e1 , . . . , en ), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i
fun valuePrim ((_, CLOSURE clo) :: vs, xi) = !applyClosureRef (clo, vs, xi)
| valuePrim _ = raise RuntimeError "primitive `value` needs a closure"
Once eval is defined, applyClosureRef can be initialized properly, to a func‐
tion that implements this rule:
ℓ1 , . . . , ℓn ∈/ dom σ ℓ1 , . . . , ℓn all distinct <+> 305f
σ̂ = σ{ℓ1 7→ v1 , . . . ℓn 7→ vn } BEGIN 688a
hBEGıN(es), ρc + {x1 7→ ℓ1 , . . . , xn 7→ ℓn }, sc , Fc , ξ, σ̂, Fi ⇓ hv; σ ′ , F ′ i . BindListLength
305e
hvalue, [h|c, CLOſURE(hx1 , . . . , xn i, es, sc , ρc , Fc )|i, v1 , . . . , vn ], ξ, σ, Fi ⇓p BLOCK 688a
hv; σ ′ , F ′ i CLASS 686c
type class 686c
(VALUEPRıMıTıVE)
className S588a
CLOSURE 686a
691c. hevaluation, basis, and processDef for µSmalltalk 688bi+≡ (S548c) ◁ 688b 693c ▷ type env 304
applyClosure : closure * value list * value ref env ‑> value eval 688b
type exp 688a
fun applyClosure ((formals, body, rho_c, superclass, frame), vs, xi) = find 305b
eval (BEGIN body, rho_c <+> mkEnv (formals, map ref vs), superclass, type frame S590a
frame, xi) instanceVars
686b
handle BindListLength =>
InternalError
raise RuntimeError ("wrong number of arguments to block; expected " ^
S219e
"(<block> " ^ valueSelector formals ^ " " ^ LITERAL 688a
spaceSep formals ^ ")") mkBlock S561a
val () = applyClosureRef := applyClosure mkEnv 305e
mkInteger 698a
Evaluating literal and value forms A LITERAL form represents a literal integer or mkSymbol 698a
symbol, and it is evaluated by calling mkInteger or mkSymbol. These functions can‐ type name 303
nilValue 696c
not be called safely until after the initial basis has been read and the interpreter has
NotFound 305b
been bootstrapped (Section 10.11.6, page 697); for that reason, integer and symbol NUM 686a
literals in the initial basis may appear only inside method definitions. rho 688b
RuntimeError
S213b
hLıTERAL(NUM(n)), ρ, csuper , F, ξ, σ, Fi ⇓ hh|σ(ξ(SmallInteger)), NUM(n)|i; σ, Fi spaceSep S214e
(LıTERALNUMBER) superclass 688b
SYM 686a
type value 685
hLıTERAL(ſYM(s)), ρ, csuper , F, ξ, σ, Fi ⇓ hh|σ(ξ(Symbol)), ſYM(s)|i; σ, Fi valueSelector
S587c
(LıTERALSYMBOL) xi 688b
691d. hfunction ev, the evaluator proper 689ai+≡ (688b) ◁ 691a 692a ▷
| ev (LITERAL c) =
(case c of NUM n => mkInteger n
| SYM s => mkSymbol s
| _ => raise InternalError "unexpected literal")
By contrast, a VALUE form may be evaluated safely at any time; it evaluates to
the value it carries.
(VALUE)
hVALUE(v), ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ, Fi
10 692a. hfunction ev, the evaluator proper 689ai+≡ (688b) ◁ 691d 692b ▷
| ev (VALUE v) = v
Smalltalk and Reading and writing variables The VAR and SET forms are evaluated as we would
objectorientation expect; they use the local and global environments in the same way as Impcore.
692 x ∈ dom ρ ρ(x) = ℓ
(VAR)
hVAR(x), ρ, csuper , F, ξ, σ, Fi ⇓ hσ(ℓ); σ, Fi
692c. hfunction ev, the evaluator proper 689ai+≡ (688b) ◁ 692b 692d ▷
| ev (SUPER) = ev (VAR "self")
(EMPTYBEGıN)
hBEGıN(), ρ, csuper , F, ξ, σ, Fi ⇓ hnil ; σ, Fi
Most definitions are evaluated more or less as in other interpreters, but class defi‐
BEGIN 688a
nitions require a lot of special‐purpose code for creating classes. BLOCK 688a
CLASSD 687b
Function evaldef, for evaluating definitions type def 687b
DEFINE 687b
emptyEnv 305a
Evaluating a definition computes a new global environment, and it also has a side
type env 304
effect on the state of the interpreter. ev 689a
693c. hevaluation, basis, and processDef for µSmalltalk 688bi+≡ (S548c) ◁ 691c S548e ▷ eval 688b
EXP 687b
evaldef : def * value ref env ‑> value ref env * value
type exp 688a
ev : exp ‑> value
find 305b
fun evaldef (d, xi) = METHOD 688a
let fun ev e = eval (e, emptyEnv, objectClass, noFrame, xi) mkCompiledMethod
hhandle unexpected Return in evaldef S590ei S570c
newClassObject
val (x, v) =
695a
case d
nilValue 696c
of VAL (name, e) => (name, ev e) noFrame S590b
| EXP e => ("it", ev e) NotFound 305b
| DEFINE (name, args, body) => (name, ev (BLOCK (args, [body]))) objectClass 696a
| CLASSD (d as {name, ...}) => (name, newClassObject d xi) optimizedBind
val xi' = optimizedBind (x, v, xi) S587b
PRIMITIVE 688a
in (xi', v)
primitives S550d
end
rho 688b
This implementation is best understood in four steps: RuntimeError
S213b
1. Define ev to evaluate an expression e in the context of a message sent to an SET 688a
SUPER 688a
instance of class Object: use an empty ρ, use Object as the superclass, and VAL 687b
use the given xi. And because e is not in a method, there is no method in‐ VALUE 688a
vocation to which it can return, so its current frame F is given as noFrame, type value 685
which is different from any allocated frame. VAR 688a
xi 688b
2. Analyze the definition d to find the name x being defined and to compute the
value v that x stands for. Depending on the form of the definition, v may be
the result of evaluating an expression, or it may be a new class object.
3. Compute the new environment ξ ′ .
mkClass : name ‑> metaclass ‑> class ‑> name list ‑> method list ‑> class
methodDefns : class * class ‑> method_def list ‑> method list * method list
setMeta : class * class ‑> unit
className : class ‑> name
10 classId
methodName
methodsEnv
: class ‑> metaclass ref
: method ‑> name
: method list ‑> method env
findClassAndMeta : name * value ref env ‑> class * class
Smalltalk and
objectorientation Figure 10.33: Utilities for manipulating classes (from the Supplement)
694
4. Return ξ ′ and v.
Evaluating a VAL or EXP form evaluates the given expression and binds it to a name—
either the given name or "it". DEFINE is syntactic sugar for a definition of a block.
And evaluating a class definition binds a new class object, as described below.
In both concrete and abstract syntax, a class definition can define methods in two
flavors: instance method or class method. But in the operational semantics and
at run time, there is only one flavor: “method.” What’s up with that? There truly
is just one mechanism for dynamic dispatch, regardless of whether a message is
sent to a class or to an instance. The distinction between instance method and
class method is implemented by creating an extra, hidden class for each class in the
system: a metaclass. Metaclasses aren’t needed for writing typical Smalltalk code,
but if you want to know how the system implements the two flavors of methods or
how it creates new objects, metaclasses are essential.
Metaclasses are governed by these invariants:
The invariants dictate that classes and metaclasses be linked in memory in cir‐
cular ways: because every class points both to its superclass and to its metaclass,
the graph of class and metaclass objects has a cycle. By itself, the “subclass‐of” rela‐
tion has no cycles, but the “instance‐of” relation has a cycle, and the combined re‐
lation has an additional cycle. The cycles are implemented by using mutable state:
every class object is first created with a metaclass pointer that is PENDING; then its
metaclass object is created; and finally the original class is mutated to point to the
new metaclass.
The representation of class objects is defined in code chunk 686c, but the large
record type is awkward to manipulate directly. Instead, functions that manipulate
ML values of type class do so using utility functions that are defined in Appendix U
and are summarized in Figure 10.33 on the facing page.
A typical class object and its corresponding metaclass are both created by func‐
tion newClassObject, which is given the definition of the class. The new class has a
superclass, which is found (along with its metaclass) by function findClass, which
looks up the name of the superclass in environment ξ . The method definitions are
§10.11
segrated into class methods and instance methods by function methodDefns, which
The interpreter
also attaches the correct static superclass to each method. The new class is built
by mkClass, its metaclass is built by mkMeta, and the final class object is built by 695
classObject.
695a. hdefinition of newClassObject and supporting functions 695ai≡ (S548c)
fun newClassObject {name, super, ivars, methods} xi =
let val (super, superMeta) = findClassAndMeta (super, xi)
handle NotFound s =>
raise RuntimeError ("Superclass " ^ s ^ " not found")
val (cmethods, imethods) = methodDefns (superMeta, super) methods
val class = mkClass name PENDING super ivars imethods
val () = setMeta (class, mkMeta class cmethods)
in classObject class
end
Function classObject uses CLASSREP to make the class a rep, then pairs it with
the class of which it is an instance, that is, its metaclass. Any attempt to refer to an
uninitialized metaclass results in a checked run‐time error.
695b. hmetaclass utilities 695bi≡ (S550c) S588d ▷
metaclass : class ‑> class
classObject : class ‑> value CLASS 686c
type class 686c
fun metaclass (CLASS { class = ref meta, ... }) = classClass 696d
case meta of META c => c className S588a
| PENDING => raise InternalError "pending class" CLASSREP 686a
findClassAndMeta
fun mkMeta c classmethods = mkMeta : class ‑> method list ‑> class
10 The exceptions are the four primitive classes, which are created using ML code:
• Object is primitive because it has no superclass.
The hmethods of class Object 634i are defined throughout this chapter and in Ap‐
pendix U, starting in chunk 634.
Class UndefinedObject, whose sole instance is nil, redefines isNil, notNil,
and print, as shown in chunks 654 and S559d.
696b. hbuiltin class UndefinedObject and value nilValue 696bi≡ (S550c) 696c ▷
val nilClass =
mkClass "UndefinedObject" PENDING objectClass []
(internalMethods hmethods of class UndefinedObject, as strings (from chunk 654)i)
Class UndefinedObject has a single instance, internally called nilValue. To en‐
able it to be returned from some primitives, it is created here.
696c. hbuiltin class UndefinedObject and value nilValue 696bi+≡ (S550c) ◁ 696b
val nilValue =
let val nilCell = ref (nilClass, USER []) : value ref
val nilValue = (nilClass, USER (bind ("self", nilCell, emptyEnv)))
val _ = nilCell := nilValue
in nilValue
end
Class Class is in the interpreter so that metaclasses can inherit from it, and
Metaclass is here so that each metaclass can be an instance of it.
696d. hbuiltin classes Class and Metaclass 696di≡ (S550c)
val classClass =
mkClass "Class" PENDING objectClass []
(internalMethods hmethods of class Class, as strings (from chunk 697a)i)
val metaclassClass =
mkClass "Metaclass" PENDING classClass []
(internalMethods hmethods of class Metaclass, as strings (from chunk 697b)i)
Most of the methods of class Class are relegated to Appendix U, but the default
implementation of new is shown here:
697a. hmethods of class Class 697ai≡ S559b ▷
(method new () (primitive newUserObject self))
For metaclasses, this default is overridden; a metaclass may not be used to instan‐
tiate new objects.
697b. hmethods of class Metaclass 697bi≡
(method new () (self error: 'a‑metaclass‑may‑have‑only‑one‑instance)) §10.11
Internal classes classClass and metaclassClass are used in the implementation The interpreter
of mkMeta, shown above in chunk 695d. Once mkMeta is defined, it is used to create 697
the metaclasses of classes Object, UndefinedObject, Class, and Metaclass.
697c. hmetaclasses for builtin classes 695ci+≡ (S550c) ◁ 695d
fun patchMeta c = setMeta (c, mkMeta c [])
val () = app patchMeta [objectClass, nilClass, classClass, metaclassClass]
In most languages, literal integers, Booleans, and nil would be simple atomic val‐
ues. But in Smalltalk, they are objects, every object has a class, and relations among
objects and classes include circular dependencies:
1. When the evaluator sees an integer literal, it must create an integer value.
2. That value must be an instance of class SmallInteger.12
3. Class SmallInteger is defined by µSmalltalk code.
4. That code must be interpreted by the evaluator.
Function valOf and exception Option are part of the initial basis of Standard ML.
Once the predefined class definitions have been read, the reference cells are
updated by function saveLiteralClasses, which takes one parameter, the global
environment xi.
698b. hsupport for bootstrapping classes/values used during parsing 698ai+≡ (S547) ◁ 698a 699a ▷
Cycles of blocks The same drill that applies to literal expressions also applies to
blocks: Evaluating a block expression creates an object of class Block, which also
is not defined until its definition is read. Blocks are supported by these functions
defined in Appendix U:
mkBlock : name list * exp list * value ref env * class * frame ‑> value
saveBlockClass : value ref env ‑> unit
sameObject className addWithOverflow value
class protocol subWithOverflow printu
isKindOf localProtocol mulWithOverflow printSymbol
isMemberOf getMethod + newSymbol
error setMethod ‑ arrayNew
subclassResponsibility removeMethod * arraySize
leftAsExercise methodNames div arrayAt
newUserObject newSmallInteger < arrayUpdate
superclass printSmallInteger > §10.11
The interpreter
Figure 10.34: µSmalltalk’s primitives
699
Booleans Booleans also participate in a cycle, but it’s the Boolean objects, not the
Boolean classes, that must be bootstrapped. Each Boolean object is stored in its
own mutable reference cell.
699a. hsupport for bootstrapping classes/values used during parsing 698ai+≡ (S547) ◁ 698b S561a ▷
local mkBoolean : bool ‑> value
val trueValue = ref NONE : value option ref
val falseValue = ref NONE : value option ref
in
fun mkBoolean b = valOf (!(if b then trueValue else falseValue))
handle Option => raise InternalError "uninitialized Booleans"
fun saveTrueAndFalse xi =
( trueValue := SOME (!(find ("true", xi)))
; falseValue := SOME (!(find ("false", xi)))
)
end
10.11.7 Primitives
The Smalltalk language is so small and simple that µSmalltalk can model almost
all of it: assignment, message send, block creation, and nonlocal return. But
Smalltalk‐80 is not just a programming language. It is a programming system, uni‐
fying elements of programming language and operating system. And it is also
an interactive programming environment. Neither of these aspects is captured
in µSmalltalk, which is more of a Unix‐style programming language: µSmalltalk
programs are stored in a file system and are run by a standalone interpreter.
Smalltalk‐80, the programming system, is described in the first part of this sec‐
tion. Smalltalk‐80, the programming environment, is not described here, because
a book is not the right medium. (Two excellent Smalltalk environments, Squeak
and Pharo, are readily available for download; I urge you to explore them.) Finally,
in the rest of this section, Smalltalk‐80’s language and classes are compared with
µSmalltalk’s language and classes.
Like all of the bridge languages, µSmalltalk fits comfortably into the development
paradigm made popular by Unix:
• Source code is edited and organized by programs that are unrelated to the
interpreter or compiler, except they share a file system.
• When code changes, programs that use the code are restarted from the be‐
ginning.
• Variables and objects don’t need to be initialized before code runs; they re‐
tain their (persistent) state from the time they were first created. And when
code finishes running—that is, when a message send terminates—the inter‐
nal state of the relevant objects is (still) part of the image’s state, so it persists;
nothing is lost.
• Source code is just data, and it need not live in files. It is stored in instance
variables of objects, and it is edited by sending messages to objects—usually
by means of the graphical user interface.
This image‐based paradigm has deep consequences for language design. Most
dramatically, Smalltalk‐80 doesn’t have definition forms! To create a new class,
for example, you don’t evaluate a definition form; instead, you send a message
like subclass:instanceVariableNames: to the superclass of the class you wish
to create. It responds with a class object, to which you add methods by sending
addSelector:withMethod: or something similar. Or more likely, you manipulate
bind 305d
a graphical user interface which sends these messages on your behalf.
CLASS 686c
type class 686c
10.12.2 Smalltalk80’s language classPrim S555e
emptyEnv 305a
type env 304
More literals and classes find 305b
nilValue 696c
Smalltalk‐80 provides all the forms of data that µSmalltalk does, and more besides. USER 686a
And more classes, including classes for characters and strings, can be instantiated type value 685
by evaluating literal expressions:
Class Literal
Integer 230
Character $a
Float 3.39e-5
Symbol #hashnotquote
String ’s p a c e s’
Array #($a $b $c)
For symbols and arrays, Smalltalk‐80 uses the hash mark, not our quote mark.
class name Shape
superclass Object
instance variable names center
radius
10 class methods
instance creation
new
Smalltalk and ↑super new center: (CoordPair x: 0 y: 0) radius: 1
objectorientation
instance methods
702 observing locations
location: pointName
↑center + ((pointVectors at: pointName) * radius)
locations: pointNames
| locs |
pointNames do: [:point | locs add: (self location: point)].
↑locs
mutators
adjustPoint: pointName to: location
center ← center + (location - (self location: pointName))
scale: k
radius ← radius * k
drawing
drawOn: picture
self subclassResponsibility
private
center: c radius: r
center ← c.
radius ← r
• Each method definition is introduced by a line set in bold type; this line,
which is called the message pattern, gives the name of the method and the
names of its arguments. A method that expects no arguments has a name
composed of letters and numbers. A method that expects one argument has
a colon after the name; the name of the method is followed by the name of
the argument. When a method expects more than one argument, the name
of the method is split into keywords: each keyword ends with a colon and is
followed by the name of one argument. This concrete syntax matches the
concrete syntax of message sends, as shown below.
When a method has two or more arguments, it is named by concatenating the
keywords; thus, the private message is “center:radius:,” just as in µSmalltalk.
• The display indents the bodies of the methods. The body of a method con‐
tains a sequence of statements, which are separated by periods. To return a
value from a method, Smalltalk‐80 uses return, which is written using the
up arrow ↑ or caret ^. A method that doesn’t evaluate a return answers self,
but such a method is likely to be evaluated only for its effects, not its answer.
• The “←” symbol is used for assignment. (Modern implementations use :=.)
In Smalltalk‐80, a class object has its own instance variables, which are distinct
from the instance variables of its instances. These variables are called class instance
variables. Class instance variables are specific to a class, and their values are not
shared with subclasses—they are, quite simply, the instance variables of the meta‐
class. Smalltalk‐80 also has class variables, which, unlike instance variables or class
instance variables, are shared: they are accessible to all the instances of a class and
its subclasses, as well as the class itself and its subclasses. Class variables can make
global variables unnecessary: because every class inherits from Object, the class
variables of class Object can play the role of global variables. So in full Smalltalk,
for example, the global point‑vectors dictionary (page 622) would instead be a
class variable of class Shape.
Restricted primitives
message-pattern
<primitive n>
expressions
When the method is invoked, it executes the primitive numbered n. If the primitive
fails, the expressions are executed; if the primitive succeeds, its result is returned
and the expressions are ignored.
Primitives are not used by application programmers; as in µSmalltalk, they are
used only in methods of predefined classes.
Smalltalk‐80 comes with many, many class definitions. Only the Number and Col-
lection subhierarchies are discussed here; these general‐purpose classes are used
by many programmers. Special‐purpose hierarchies, including some that support
graphics, compilation, and process scheduling, are beyond the scope of this book.
Smalltalk80’s numbers
In Smalltalk‐80, the part of the class hierarchy that contains numeric classes, which
is the model for µSmalltalk, looks like this:
Object
§10.12
Magnitude Smalltalk
as it really is
Date Time Number 705
asLargeInteger
self negative
ifTrue: [↑self asLargeNegativeInteger]
ifFalse: [↑self asLargePositiveInteger]
Smalltalk‐80’s Collection hierarchy looks a bit different from the Collection hi‐
erarchy in µSmalltalk. The µSmalltalk hierarchy is derived from the one in Tim
Budd’s (1987) Little Smalltalk. The Smalltalk‐80 collection hierarchy looks, in part,
like this:
Object
10 Collection
Set SequenceableCollection
Smalltalk and
Dictionary LinkedList ArrayedCollection
objectorientation
Array
706
Both the Smalltalk‐80 and the µSmalltalk collection hierarchies use iteration (do:)
as the fundamental operation.
In µSmalltalk, when add: is sent to a collection, the collection answers itself. This
protocol supports an idiom that is common in object‐oriented languages, which is
to modify a collection by sending it a sequence of messages. For example,
This idiom applies to all sorts of mutable abstractions. But in Smalltalk‐80, the add:
message doesn’t work with this idiom: add: answers the object just added, not the
collection receiving the message. What gives?
It turns out that the idiom of sending a sequence of messages to a mutable
abstraction is so useful that Smalltalk‐80 provides special syntax for it: the mes
sage cascade. A receiver can be followed by a sequence of messages, separated by
semicolons, and each message is sent in sequence to the same receiver. In Squeak
Smalltalk, for example, the idiom works like this:
Reflection
10.14 SUMMARY
ABſTRACT CLAſſ A CLAſſ used only to define METHODſ, not to create ıNſTANCEſ.
Other classes ıNHERıT from it.
CLAſſ METHOD A method that responds to messages sent to a class, rather than to
instances of that class. Used most often to create or initialize instances of the
class, as with the message new.
COLLECTıON One of several Smalltalk classes whose objects act as containers for
other objects. Predefined collections include dictionaries, sets, lists, and ar‐
rays. A collection can be defined by just a handful of methods, of which the
most important is the do: method, which iterates over the objects contained
in the collection. The collection then inherits a large number of useful meth‐
ods from class Collection.
COMPLEX METHOD A METHOD that wishes to inspect the representation of one or
more ARGUMENTſ, not just the RECEıVER.
MAGNıTUDE A quantity like a date, a time, or a number, of which it can be said that
one precedes another, but that might not support arithmetic.
MEſſAGE NOT UNDERſTOOD The error that occurs when an object receives a MEſ‐
ſAGE that is not in its PROTOCOL.
MEſſAGE ſELECTOR The name that, together with arguments, constitutes a MEſ‐
ſAGE.
PROTOCOL The set of MEſſAGEſ to which an OBȷECT can respond, together with
the rules that say what actual parameters are acceptable and how the object
behaves in response.
Smalltalk was invented to help create a computing experience that might deserve
to be called “personal.” Smalltalk was part of a flood of inventions that emerged
from the Xerox Palo Alto Research Center (PARC): the one‐person computer; the
bitmapped display; the user‐interface elements that we now call windows, menus,
and pointing; the use of abstract data types in systems programming; and many
other delights that we have long since taken for granted. PARC’s Smalltalk group
wanted to create a software system that would make the computer “an amplifier for
the human reach,” not just a tool for building software systems. Alan Kay’s (1993)
account of those heady days is well worth your time.
Smalltalk‐80 is described by the “blue book,” by Goldberg and Robson (1983),
which is so called because of its predominantly blue cover. The blue book intro‐
duces the language and class hierarchy. It also presents five chapters on simula‐
tion and five chapters on the implementation and the virtual machine on which it
is based. The blue book strongly influenced the design of µSmalltalk, especially its
blocks. The “red book” (Goldberg 1983) describes the Smalltalk‐80 programming
environment and its implementation. A “green book” describes some of the early
history of Smalltalk (Krasner 1983), and Ingalls (2020) describes the evolution of
the language and its implementation from Smalltalk‐72 onward.
Smalltalk was the subject of an August 1981 special issue of Byte magazine
(Byte 1981). The issue includes twelve articles on the system, written by the peo‐
ple who built it. Topics range from design principles to engineering details, with
plenty of examples, plus a description of the programming environment. A nar‐
10 row, deep introduction to the Smalltalk‐80 language and programming environ‐
ment is presented by Kaehler and Patterson (1986), who develop a single, simple
example—the towers of Hanoi—all the way up to an animated graphical version.
If you want to use a Smalltalk system, Ingalls et al. (1997) describe Squeak,
Smalltalk and
a free, portable implementation of Smalltalk that is written in Smalltalk itself.
objectorientation
To get started, you will want help with the integrated programming environment,
712 which is otherwise overwhelming; consult Black et al. (2009). Or you could try
Pharo (Ducasse et al. 2016), a fork that looks better to modern eyes.
If you want something simpler than Smalltalk‐80 but more ambitious than
µSmalltalk, Budd (1987) describes “Little Smalltalk,” which is nearly identical to
Smalltalk‐80, but which lacks some extras, especially graphics. Budd’s book is eas‐
ier reading than the blue book, and interpreters for Little Smalltalk are available.
No book tells you everything you need to know to become an effective Smalltalk
programmer. You must master the class hierarchy, and to gain mastery, you must
read and write code. As you write, be guided by Beck (1997), who discusses coding
style; Beck’s excellent book will help you make your Smalltalk code idiomatic.
10.15 EXERCıſEſ
The exercises are arranged by the skill or knowledge they call on (Table 10.36 on
the following page). More than in other chapters, the exercises call on knowledge
of µSmalltalk’s large initial basis. Highlights include new numeric classes and an
enhancement to the interpreter.
• Exercises 37 to 39 are the most ambitious exercises in this chapter. You will
implement a classic abstraction: full integers of unbounded magnitude.
The project is divided into three stages: natural numbers, signed large in‐
tegers with arithmetic, and finally “mixed arithmetic,” which uses small ma‐
chine integers when possible and transparently switches over to large inte‐
gers when necessary. These exercises will boost your object‐oriented pro‐
gramming skills, and you will understand, from the ground up, an imple‐
mentation of arithmetic that should be available in every civilized program‐
ming language.
Table 10.36: Synopsis of all the exercises, with most relevant sections
10 1
2 to 5
10.1
10.1
Using shape classes.
Defining new shapes and new canvases.
6 and 7 10.1 Creating new classes from scratch (for random
numbers, see also §10.4.6).
Smalltalk and 8 and 9 10.3.4 Messages to super: their use for initialization, and
objectorientation their semantics.
714 10 10.4.1 Equality and object identity.
11 and 12 10.4.2 Redefinition of existing methods using the protocol
for all classes: print methods for pictures (§10.4.5);
pictures with floating‐point coordinates (§10.4.6).
13 to 15 10.4.2 New methods on existing classes: identify
metaclasses (§10.11.4), condition on nil, hash.
16 10.6 Conditionals as dynamic dispatch: the
implementation of class False.
17 to 31 10.4.5, 10.7 Modifying existing collections and defining new
collections.
32 and 33 10.4.6, 10.7.1, Make class Char a magnitude (§10.4.2); define new
10.8.2 magnitudes.
34 to 39 10.4.6, 10.8 Numbers and arithmetic: reasoning about overflow;
arithmetic between different classes of numbers;
arbitrary‐precision arithmetic.
40 and 41 10.10.1, Method dispatch: understanding its semantics;
10.11.2 improving its implementation using a method cache.
42 10.11.2 Object‐oriented profiling: finding hot spots by
measuring what objects receive what messages.
• Smalltalk’s method dispatch might seem inefficient. But many call sites dis‐
patch to the same method over and over—a property that clever implemen‐
tations exploit. In Exercise 41, you implement a simple method cache and
measure its effectiveness. The results will surprise you.
A. In the expression (c location: 'East), what is the receiver? What is the mes‐
sage selector? What is the argument?
B. In the expression (c location: 'East), what is the role of the colon after the
word location? If the colon were missing, what would go wrong?
C. Why does class CoordPair have two protocols (a “class” protocol and an “in‐
stance” protocol)? What can you do with the messages in the class protocol?
What can you do with the messages in the instance protocol?
D. In a language like C++, new signifies a syntactic form. But in Smalltalk, new
isn’t special. When the Smalltalk expression (List new) is evaluated, what
happens?
E. In the expression (c location: 'East), c is an object of class Circle, and class
Circle doesn’t define a location: method. So when the expression is evalu‐
ated, what happens? How does it work?
F. What objects can the message class be sent to? What objects can the message
superclass be sent to?
G. Once a class is defined, can you go back and add, change, or remove methods?
How?
H. Message ifTrue: is sent to a Boolean, but message whileTrue: is sent to a
block. Why the difference?
I. In the drawPolygon: method implemented on class TikzCanvas, what does the §10.15
expression in square brackets evaluate to? When the resulting object is sent to Exercises
coord‑list as an argument to the do: message, what happens?
715
J. A µSmalltalk list is just one form of collection—there are others. All collections
understand messages that are analogous to the µScheme list functions map,
app, filter, exists?, and fold. What is the Smalltalk analog of each of these
list functions?
K. When message at:ifAbsent: is sent to a keyed collection, why is the second
argument a block?
L. In Smalltalk, every number is a magnitude, but not every magnitude is a num‐
ber. What’s an example of something can you do with a Number that you can’t
do with a Magnitude? What’s an example of an abstraction that is a magnitude
but not a number?
M. How is the message isNil implemented without a conditional test?
N. The Magnitude protocol offers six relational operators plus operations min: and
max:. But no individual magnitude class has to implement all of them. What
operations have to be implemented by subclasses of Magnitude, like Integer
or Fraction? And how do things work with the other Magnitude operations,
which aren’t implemented by the subclass?
O. Study how return is used in the implementations of methods isEmpty and
includes: on class Collection. Each of these methods has a body, which con‐
tains one or more nested blocks, one of which contains a return. When the
return is evaluated, what evaluation or evaluations are terminated?
P. To iterate over a list in µScheme, we have to ask the list how it was formed—is it
empty or nonempty? Iteration in Smalltalk doesn’t have to ask such questions.
How is list iteration accomplished instead?
Q. Method = on class Fraction needs to know the numerator and denominator of
both the receiver and the argument. How does the method get the numerator
and denominator of the receiver? How does it get the numerator and denomi‐
nator of the argument?
R. In the semantic judgment he, ρ, csuper , F, ξ, σ, Fi ⇓ hv; σ ′ , F ′ i, components e,
ρ, ξ , and σ have their familiar roles. What is the role of the component csuper ?
S. In the semantic judgment he, ρ, csuper , F, ξ, σ, Fi ↑ hv, F ′ ; σ ′ , F ′ i, what are
the roles of the two frames F and F ′ ? In particular, what happens if those
two frames are identical?
T. In the evaluation of an expression of the form ſEND(m, e, e1 , . . . , en ), suppose
that the evaluation of e1 returns v to frame F ′ . Are expressions e2 to en evalu‐
ated? What rules of the operational semantics determine the answer?
U. A Smalltalk class is also an object, but in the interpreter, it is represented as an
ML record. How does such an object know what its class is? What is the name
for the class of such an object?
V. A µSmalltalk literal like 1983 evaluates to an object of class SmallInteger. But
class SmallInteger is itself defined using µSmalltalk code, and that code has to
be read by the parser! How is this cyclical dependence resolved? Can anything
go wrong?
10 10.15.2 Using shape classes
(a)
(b)
(c) (The radii of the circles are in the ratio 9 to 6 to 4.)
3. Reuse code by defining and inheriting from a Polygon class. The square and tri‐
angle are both polygons, and both can be drawn in the same way, only using
different points. Define a new, abstract class Polygon, which is a subclass
of Shape, and which includes a method points. When sent the points mes‐
sage, a polygon should answer a list of symbols that name the control points
that should be drawn.
The class method new will need to be changed, but sending new to class
TikzCanvas should continue to create a canvas with a scale of 4pt.
5. A new class of canvas. In this problem, you replace TikzCanvas with a different
back end.
(b) To draw PostScript pictures, define a new class PSCanvas. It should 717
implement the same protocol as TikzCanvas, and it should produce
PostScript.
To generate the next random number, use the algorithms from page 124.
8. Correct initialization of new pictures. Section 10.3.4 says that sending new to a
class should answer a newly allocated object whose state respects the private
invariants of the class. One such invariant, of class Picture, is that instance
variable shapes refers to a list of shapes. But the definition of Picture in
Figure 10.4 simply inherits new from Object, and new returns an uninitialized
Picture object. The definition in Figure 10.4 does not include new because in
such an early example, I did not want to introduce the technique of sending
messages to super. Define new on class Picture so it creates a picture with
an empty list of shapes, by sending empty to self. Then change the empty
class method to use super.
9. Alternative semantics for super. Suppose super were implemented like this:
when sending message m to super, send it to self, but start the method
search in the superclass of self’s class. Compare this implementation with
the way super is actually implemented, and explain how they are different.
Find an example from this chapter in which the incorrect version does the
wrong thing.
You may find it expedient to implement the incorrect version.
10.15.6 Equality
10. Object identity for keys in collections. The collection classes compare keys with
equivalence (=). What if you really wanted object identity (==)? You don’t
11. Pictures with noninteger coordinates. The print method in the CoordPair
class sends the print message to instance variables x and y. As long as x and y
are integers, this works fine, but if they are any other kind of numbers, the
syntax that µSmalltalk prints won’t be recognized by the LATEX TikZ package.
In this exercise you modify both the Float and the CoordPair classes to sup‐
port shapes and positions with non‐integer values, using LATEX syntax.
(a) Redefine the print method on the Float class so that it prints self as
an integer part, followed by a decimal point, followed by two decimal
digits, as follows:
718. hexercise transcripts 718i≡ 720a ▷
‑> (69 asFloat)
69.00
‑> ((‑3 / 4) asFloat)
‑0.75
‑> ((314 asFloat) / (100 asFloat))
3.14
12. More informative print method for pictures. The print method of class
Picture would be more interesting if it showed the shapes inside the pic‐
ture, like so:
‑> pic
Picture ( <Circle> <Square> <Triangle> )
Make it so. (You might wish to study the print method for class Collection.)
10.15.8 New methods on existing classes
The exercises in the next group explore changes, extensions, and additions to the
predefined classes. To modify a predefined class, you have two choices:
14. Method ifNil: for all objects. In a full Smalltalk system, message ifNil: can
be sent to any object. The message takes one argument, which is a block.
The receiver answers itself, unless the receiver is nil, in which case it an‐
swers the result of sending value to the argument block. Define as many
ifNil: methods as you need to, then use reflection to update µSmalltalk’s
predefined classes so that every object responds appropriately to ifNil:.
15. Give every object a hash method. In µSmalltalk’s predefined classes, only ob‐
jects of class Symbol respond to a hash message. Using reflection, update the
predefined classes so that every object responds to the hash message with a
small integer. Equal objects must hash to equal values.
16. Conditional operations on class False. Study the implementation of class True
in Section 10.6. Write the corresponding definition of class False.
10.15.10 Collections
18. A class Interval of integer sequences. An object of class Interval represents a asFloat B 650
finite sequence of integers in arithmetic progression. The sequence takes
the form [n, n + k, n + 2 · k, . . . , n + m · k] for some n > 0, k > 0,
and m ≥ 0. An interval is defined by n, m, and k ; intervals are im‐
mutable. An interval is a collection and answers the same protocol as any
other sequenceable collection, except that it lacks add:. Interval’s class pro‐
tocol should provide two initializing methods: from:to: and from:to:by:.
Sending (from:to:by: Interval n (n + m · k) k ) should result in an inter‐
val containing the sequence shown above; (from:to: Interval n (n + m))
should use k = 1.
Define class Interval as a subclass of SequenceableCollection.
19. Using Interval for array indices. Use class Interval to implement methods
associationsDo:, collect:, and select: on arrays, and to re‐implement
do:.
10 20. Class method withAll: for arrays. Extend class Array with an additional class
method withAll: aCollection, which makes an array out of the elements
of another collection. To add a class method using reflection, you send
addSelector:withMethod: to Array’s metaclass, which you get by evaluating
Smalltalk and (Array class).
objectorientation
21. Methods select: and collect: for arrays. Add implementations of select:
720 and collect: to class Array.
22. Collection class Bag. Define the collection class Bag. An object of class Bag is
a grocery bag; it’s an unordered collection of elements. Unlike a set, a bag
may contain the same element multiple times, as in two cartons of milk; bags
are sometimes called multisets. A bag has the same protocol as Set, and it
also answers count: by telling how many copies of a given object it holds.
For example:
720a. hexercise transcripts 718i+≡ ◁ 718 720b ▷
‑> (val B (Bag new))
‑> (B add: 'milk)
‑> (B add: 'milk)
‑> (B add: 'macaroni)
‑> (B includes: 'milk)
<True>
‑> (B count: 'milk)
2
23. Arrays that can change size. Fixed‐size arrays are a nuisance. Using the de‐
sign in Section 9.6.2 as a model, create an ArrayList class that inherits
from SequenceableCollection and that represents arrays with indices from
n to m, inclusive. It should also support the following new messages.
• Class method new should create a new, empty array with n = 1 and
m = 0.
• Instance method addlo: should decrease n by 1 and add its argument
in the new slot. Instance method addhi: should increase m by 1 and
add its argument in the new slot. Method add: should be a synonym
for addlo:.
• Instance method remlo should remove the first element, increase n
by 1, and answer the removed element. Instance method remhi should
remove the last element, decrease m by 1, and answer the removed el‐
ement.
Each of these operations should take constant amortized time, as should at:.
For ideas about representation, look at the implementation of this abstrac‐
tion using Molecule (Appendix T).
24. Lists implemented by arrays. The ArrayList class from Exercise 23 could be
used to implement List.
(a) Estimate the space savings for large lists, in both the average case and
the worst case.
(b) Write an implementation. §10.15
Exercises
25. Complete class List.
721
(a) Implement removeLast for class List.
(b) Implement removeKey:ifAbsent: for lists.
26. Fix at:put: on class List. If the index is out of range, the at:put: method
on class List silently produces a wrong answer. What an embarrassment!
Please fix it.
28. Hash tables. Define a class Hash that implements a mutable finite map using
a hash table. Objects of class Hash should respond to the same protocol as
objects of class Dictionary, except that a Hash object may assume that the
hash message may be sent to any key. A key object answers the hash mes‐
sage with an machine integer that never changes, even if the state of the key add: B 644
changes. Choose a suitable µSmalltalk class to inherit from, and use the rep‐ do: B 644
resentation and the invariants from the hash table in Exercise 34, Chapter 9 includes: B 644
print 615
(page 598).
space B 641
29. Binarysearch trees. Define a class BST that implements a mutable finite map
using a binary search tree. Objects of class BST should respond to the same
protocol as objects of class Dictionary, except that a BST object may assume
that keys respond to the Magnitude protocol.
(a) Using removeSelector:, remove the definition of at: from class Array,
so it inherits at: from SequenceableCollection.
(b) On class Array, implement at:ifAbsent: using primitive arrayAt and
the other methods of class Array.
31. An iterator for locations in shapes. In Section 10.1, examine the algorithm for
10 drawing polygons and look at what kinds of intermediate data are allocated.
Sending locations: to an object of class Shape allocates an intermediate list
of locations, which is used in drawPolygon: on class Tikzpicture, then im‐
mediately thrown away. Eliminating this sort of allocation often improves
Smalltalk and performance.
objectorientation
Class Shape doesn’t have to provide a list of the locations; it could equally well
722 provide an iterator, with this protocol:
locationsDo:with: symbols aBlock
For each symbol s in the collection symbols, send
(aBlock value: l), where l is the location of the control point
named by symbol s. If s does not name any control point, the
result is a checked run‐time error.
(a) Extend class Shape with a locationsDo:with: method.
(b) Suppose that locations: were eliminated, so that all client code had to
use the locationsDo:with: method. What other classes would have to
change, in what ways, to continue to draw polygons?
(c) Is there a way to change the other classes so that no intermediate list
(or other collection) is allocated? If so, explain what other intermediate
objects have to be allocated instead. If not, explain why not.
(d) If your answer to the previous part is “yes, the other classes can be
changed,” then explain whether you prefer the new design or the orig‐
inal, and why.
33. More magnitudes. In µSmalltalk, the only interesting Magnitudes are Numbers.
Don’t stop at Char—define more Magnitudes!
10.15.12 Numbers
36. Mixed arithmetic with integers and fractions. This problem explores improve‐
ments in the built‐in numeric classes, to try to support mixed arithmetic.
(a) Use reflection to arrange the Fraction and Integer classes so you
can add integers to fractions, subtract integers from fractions, mul‐
tiply fractions by integers, etc. That is, make it possible to perform §10.15
arithmetic by sending an integer as an argument to a receiver of class Exercises
Fraction. Minimize the number of methods you add or change.
723
(b) Use reflection to change the Integer class so you can add fractions to
integers—that is, use a fraction as an argument to a + message sent to
an integer. This requires much more work than part 36(a). You might
be tempted to change the + method to test to see if its argument is an
integer or a fraction, then proceed. A better technique is to use double
dispatch (Section 10.8.3, page 666): the + method does nothing but send
a message to its argument, asking the argument to add self. The key is
that the message encodes the type of self. For example, using double
dispatch, the + method on Integer might be defined this way:
723. hdouble dispatch 723i≡
(method + (aNumber)
(aNumber addIntegerTo: self))
The classes Integer, Float, and Fraction would all then define meth‐
ods for addIntegerTo:.
(c) Complete your work on the Integer class so that all the arithmetic oper‐
ations, including = and <, work on mixed integers and fractions. Min‐
imize the number of new messages you have to introduce. You may
wish to use reflection to change or remove some existing methods on
Integer and SmallInteger.
(d) Finish the job by making Fraction’s methods answer an integer when‐
ever the denominator of the fraction is 1.
You might be tempted to test your code against a table of factorials, but fac‐
torials are computed using only multiplication. Try Catalan numbers, which
can be computed recursively using a combination of multiplication and ad‐
dition.
40. Formal semantics for method dispatch. Judgment form m c @ imp describes
method dispatch (Section 10.10.1). This form isn’t described by proof rules,
but it is implemented by function findMethod in chunk 690b. Using the im‐
plementation as a guide, write proof rules for the judgment.
41. Method cache. Because Smalltalk does everything with method dispatch,
method dispatch has to be fast. The cost of dispatch can be reduced by
method caching, as described by Deutsch and Schiffman (1984). Method
caching works like this: with each SEND form in an abstract‐syntax tree, asso‐
ciate a two‐word cache. One word gives the class of the last object to receive
the message sent, and the other gives the address of the method to which the
message was last dispatched. When the message is sent, consult the cache;
if the class of the current receiver is the same as the class of the cached re‐
ceiver, then execute the cached method; no method search is needed. If the
class of the current receiver is different, search for a method in the usual
way, update the cache, and execute the method.
Method caching saves time if each SEND in the source code usually sends to
an object of a single class—which research suggests is true about 95% of the
time. Adding a method cache made an early Berkeley Smalltalk system run
37% faster (Conroy and Pelegri‐Llopart 1982). §10.15
Exercises
(a) Extend the µSmalltalk interpreter with method caches.
725
• Define a type classid whose value can uniquely identify a class.
Since the class field of each class is unique, you can start with
type classid = metaclass ref
• Add a field of type (classid * method) ref to the SEND form in the
abstract syntax. This reference cell should hold a pair represent‐
ing the unique identifier of the last class to receive the message at
this call site, plus the method found on that class.
• Change the parser to initialize each cache. As the initial class iden‐
tifier, define
val invalidId : classid = ref PENDING
Value invalidId is guaranteed to be distinct from the class field
of any class.
• Change function ev in chunk 689b to use the cache before calling
findMethod, and to update the cache afterward (when a method
is found). Consider defining a function findCachedMethod, whose
type might be (classid * method) ref * name * class ‑> method.
(b) Add code to gather statistics about hits and misses, and measure the
cache‐hit ratio for a variety of programs.
(c) Find or create some long‐running µSmalltalk programs, and measure
the total speedup obtained by caching. (Because the method cache
does not eliminate the overhead of tracing, the speedup will be rela‐
tively small. You might want to suppress tracing by adding the defini‐
tion fun trace f = f ().)
(d) If a µSmalltalk program uses the setMethod primitive or any of the
reflective facilities that copy methods, it could invalidate a cache. Ei‐
ther eliminate the primitive or correct your implementation so that ev‐
ery use of the primitive invalidates caches that depend on the relevant
class. Don’t forget that changing a method on class C may invalidate
not only caches holding C but also caches holding C ’s subclasses.
Measure the cost of the additional overhead required to invalidate the
caches.
Congratulations! You now have some solid skills using functions, types, modules,
objects, and more. You also have a cognitive framework that you can use to learn
new programming languages, and if you’re like my students, you’ll be pleasantly
surprised at how broadly your skills apply. Now you get the dessert menu. As your
server, I recommend some tasty treats: languages that are superlative, unusual, or
popular. Many are widely recognized as interesting or important, and some are just
to my personal taste. Together, they offer a variety of jumping‐off points, ranging
from younger, less proven ideas to fashionable ideas drawn from the headlines.
TYPEFUL PROGRAMMıNG
If you like types, try Haskell. Haskell is a pure functional language: even I/O inter‐
actions are represented as values with special IO types. And Haskell is pushing new
type‐system ideas into the mainstream far faster than any other language. By the
time this book is published, Haskell will have cool new features I haven’t even heard
of yet. Start with the basics: type classes and monads (Lipovača 2011). Definitely
try QuickCheck (Claessen and Hughes 2000; Hughes 2016), which uses Prolog‐like
inference rules to do amazing things with random testing. And don’t overlook
QuickCheck’s shrink function—although it’s barely mentioned in the original pa‐
per, it’s a crucial part. After that, explore what Stephanie Weirich and Richard
Eisenberg are doing.
Dependent types can express many interesting properties of data. The classic ex‐
ample uses types to keep track of how long a list is, without losing polymorphism.
Interesting dependently typed languages include Idris and Agda. The Epigram lan‐
guage is less active, but its papers and tutorials are very good (McBride 2004).
Types are also being used to manage memory: types can enable safe, fast sys‐
tems programming without (much) garbage collection. Look into Rust, and check
out earlier work on Cyclone (Grossman et al. 2005).
PROPOſıTıONſ Aſ TYPEſ
Early in the twenty‐first century, proofs about correctness start with the principle
of propositions as types.
Propositions in classical logic correspond to types in a functional language (See
Table AW.1 on the next page). In logic, symbols A and B represent propositions;
727
Table AW.1: Elements of classical logic with their counterparts in types
Proposition Type
Conjunction A∧B A×B Pair
Disjunction A∨B A+B Sum
Implication A⊃B A→B Function
Complement ¬A — (no counterpart)
Quantification ∀α.A ∀α.A Polymorphism
Afterword Truth > 1 Unit
728 Falsehood ⊥ void Uninhabited
T `A T `B Γ ` e1 : A Γ ` e2 : B
.
T `A∧B Γ ` (e1 , e2 ) : A × B
In logic, symbol T represents a theory, and it lists all the propositions whose truth
is assumed. In language, symbol Γ represents a typing context, and it lists all the
types whose inhabitation is assumed. (The context gives a variable of each type,
and the type is inhabited by the value of that variable.)
As another example, in logic, A ⊃ B (implication) is proved by assuming A,
then using the assumption of A to prove B . In language, a value of type A → B is
created by defining a function that takes a value of type A, then uses the value to
produce a result of type B . Again, the derivations end in analogous steps:
T, A ` B Γ, x : A ` e : B
.
T `A⊃B Γ ` λx : A.e : A → B
Almost every classical logical connective corresponds to a type constructor, ex‐
cept one: logical complement. Programming‐language types correspond to what
is called constructive or intuitionistic logic, which doesn’t have complement. Like
Prolog (Appendix D), constructive logic concerns itself not with what is true, but
with what is provable. And the provable is the computable.
MORE FUNCTıONſ
MORE OBȷECTſ
Objects are everywhere. If you know Smalltalk, you almost know Ruby, but the pro‐
gramming environment is very different. The difference is this: Squeak Smalltalk
gives you a lovely graphical programming environment, but it’s not really con‐
nected to anything else on your computer. It’s like having a fabulous house—
on Mars. Ruby gives you the same fabulous language, but right next door. Or you
could try Pharo, which nicely integrates the Smalltalk language and programming
environment into a modern operating system.
Then there are the hybrids. Objective C seems most faithful to Smalltalk’s vi‐
sion. If you prefer complexity to simplicity, you’re ready for Python. Add more
static types and you have Java, C#, C++, Swift, and many other popular languages.
More interesting languages provide objects with prototypes instead of classes.
Self is the original and a great place to start (Ungar and Smith 1987). Once you have
the ideas, you can apply them in JavaScript, whose good parts combine ideas from
Scheme and Self (Crockford 2008), and in Lua, a great scripting language mentioned
below.
Lots of designers have put functions and objects together in a single language.
OCaml was an early player, combining what was then known about objects and
functions into a single system. More ambitious efforts include Scala and F#, each
of which integrates an existing object‐oriented type system (respectively the Java
Virtual Machine and the Microsoft Common Language Runtime) with ideas from
functional programming.
FUNCTıONAL ANıMATıON
SCRıPTıNG
730 If you want programs to run in parallel, and especially if you want programs to
run distributed over multiple computers, try Erlang. It’s got a simple, effective
message‐passing model of concurrency, plus great ideas about recovering from
failure—and if you’re distributing computations, you have to worry about failure.
And Erlang is easy to pick up; it uses Prolog’s syntax and data, but its computational
model is a lot like Scheme.
For more message‐passing concurrency, but with a very different flavor, try Go.
Go’s primary mission is to “make systems programming fun again,” and the concur‐
rency model is sweet.
The language Inform7 is designed for writing interactive fiction, or, as we used
to call it, text adventure games (ATTACĸ DRAGON WıTH BARE HANDſ and all that).
An Inform7 program doesn’t even look like a program; it looks like a cross between
a manual and a text adventure game. And Inform7 ships with a really interesting
interactive programming environment. I’m still wondering exactly what the ab‐
stract syntax of Inform7 is—or if it even uses an abstract syntax—but if you have
any interest in this domain, you have to try it.
STACĸ‐BAſED LANGUAGEſ
Chapter 3 uses an evaluation stack to implement µScheme+, but the stack is hid‐
den from the programmer. Why not expose it? The classic stack‐based language is
Forth, which gets extraordinary power from a minimal design. A Forth environ‐
ment can fit in a few kilobytes of RAM, and Forth has been used on many embedded
systems. For a nice example, check out the Open Firmware project.
If you like your stacks with a few more data types, write some PostScript. Sweep
away all the primitives related to fonts and graphics, and you’ll find a very nice
stack‐based language underneath. Even the current environment is represented
as a stack, and by manipulating it, you can quickly change the meanings of names
en masse.
ARRAY LANGUAGEſ
While John McCarthy was developing his list‐based language, Ken Iverson (1962)
was developing the array‐based language APL. (Both later received Turing Awards.)
Derived from notation used at the blackboard, APL evolved into an important lan‐
guage at IBM, where it was written by using a special typeball on IBM’s Selectric
typewriter. At a time when almost all IBM’s mainframe business meant program‐
ming with punched cards, APL gave IBM a powerful interactive option. APL orig‐
inated many ideas that were fully developed in functional languages, including
maps, filters, and folds. You can also check out Iverson’s successor language, J,
which may look like line noise, but which runs on ordinary computers. Both APL
and J enable you to define array functions that are polymorphic not just in the size
of an array, but in the number of its dimensions. This behavior can be explained
using a modern, static type system of the sort you have mastered (Slepak, Shivers,
and Manolios 2014).
STRıNG‐PROCEſſıNG LANGUAGEſ
Domain‐specific languages for string processing are in decline, but Icon is worth
looking at: it uses a string scanning technique that relies on a backtracking eval‐
uation model just like the one used in Prolog. Unlike regular expressions, string
scanning can be extended with custom string‐processing abstractions, and they
compose nicely with the built‐in abstractions.
For historical interest only, I recommend Awk, which was important early and
for a long time, although it is both dominated and superseded by later languages.
The code I once wrote in Awk I now write in Lua.
Also relegated to historical interest is Perl, which has first‐class functions like
Scheme, multiple name spaces like Impcore, and string processing based on regu‐
lar expressions. Perl has become unfashionable, but there is plenty of legacy Perl
code out there—to which your skills apply.
CONCLUſıON
733
Nick Benton and Andrew Kennedy. 2001 Frederick P. Brooks, Jr. 1975. The Mythi
(July). Exceptional syntax. Journal of cal ManMonth. Reading, MA: Addison‐
Functional Programming, 11(4):395–410. Wesley.
Richard Bird and Philip Wadler. 1988. Intro Carl Bruggeman, Oscar Waddell, and R. Kent
duction to Functional Programming. New Dybvig. 1996 (May). Representing con‐
York: Prentice Hall. trol in the presence of one‐shot continua‐
G. M. Birtwistle, O.‐J. Dahl, B. Myhrhaug, tions. Proceedings of the ACM SIGPLAN ’96
and K. Nygaard. 1973. Simula Begin. New Conference on Programming Language De
York: Van Nostrand‐Reinhold. sign and Implementation, in SIGPLAN No
Bibliography Andrew Black, Stéphane Ducasse, Oscar tices, 31(5):99–107.
Nierstrasz, Damien Pollet, Damien Cas‐ Timothy Budd. 1987. A Little Smalltalk. Read‐
734 sou, and Markus Denker. 2009. Squeak ing, MA: Addison‐Wesley.
by Example. Square Bracket Associates. W. H. Burge. 1975. Recursive Program
Available under a Creative Commons li‐ ming Techniques. Reading, MA: Addison‐
cense. Wesley.
Andrew P. Black, Kim B. Bruce, Michael R. M. Burstall, J. S. Collins, and R. J. Popple‐
Homer, and James Noble. 2012. Grace: stone. 1971. Programming in POP2. Edin‐
The absence of (inessential) difficulty. In burgh: Edinburgh University Press.
Proceedings of the ACM International Sym Rod M. Burstall, David B. MacQueen, and
posium on New Ideas, New Paradigms, and Donald T. Sannella. 1980 (August). Hope:
Reflections on Programming and Software, An experimental applicative language. In
Onward! 2012, pages 85–98. Conference Record of the 1980 LISP Confer
Andrew P. Black, Stéphane Ducasse, Oscar ence, pages 136–143.
Nierstrasz, and Damien Pollet. 2010. Paris Buttfield‐Addison, Jon Manning, and
Pharo by Example (Version 20100201). Tim Nugent. 2016. Learning Swift: Build
Square Bracket Associates. ing Apps for OS X and iOS. O’Reilly.
Stephen M. Blackburn, Perry Cheng, and Lawrence Byrd. 1980. Understanding the
Kathryn S. McKinley. 2004 (June). Myths control flow of Prolog programs. In
and realities: The performance impact of S.‐A. Tarnlund, editor, Proceedings of the
garbage collection. SIGMETRICS Perfor Logic Programming Workshop, pages 127–
mance Evaluation Review, 32(1):25–36. 138. See also University of Edinburgh
D. G. Bobrow, L. G. DeMichiel, R. P. Gabriel, Technical Report 151.
S. E. Keene, G. Kiczales, and D. A. Moon. Byte. 1981 (August). Special issue on
1988 (September). Common Lisp object Smalltalk. Byte Magazine, 6(8). Can be
system specification. ACM SIGPLAN No downloaded from archive.org.
tices, 23(SI). Howard I. Cannon. 1979. Flavors: A non‐
Daniel G. Bobrow, Ken Kahn, Gregor Kicza‐ hierarchical approach to object‐oriented
les, Larry Masinter, M. Stefik, and F. Zdy‐ programming. Unnumbered, draft tech‐
bel. 1986 (November). Common Loops, nical report, variously attributed to the
merging Lisp and object‐oriented pro‐ MIT AI Lab or to Symbolics, Inc.
gramming. ACM SIGPLAN Notices, 21(11): Luca Cardelli. 1987 (April). Basic polymor‐
17–29. phic typechecking. Science of Computer
Hans‐Juergen Boehm and Mark Weiser. 1988 Programming, 8(2):147–172.
(September). Garbage collection in an Luca Cardelli. 1989 (February). Type‐
uncooperative environment. Software— ful programming. In E. J. Neuhold and
Practice & Experience, 18(9):807–820. As of M. Paul, editors, Formal Description of Pro
2022, this collector continues to be main‐ gramming Concepts, IFIP State of the Art
tained; see https://www.hboehm.info/ Reports Series. Springer‐Verlag. Also ap‐
gc/. peared as DEC SRC Research Report 45.
George Boole. 1847. The mathematical analy Luca Cardelli. 1997. Type systems. In
sis of logic. Cambridge: Macmillan, Bar‐ Allen B. Tucker, Jr., editor, The Computer
clay, & Macmillan. Project Gutenberg Science and Engineering Handbook, chap‐
ebook number 36884. ter 103, pages 2208–2236. Boca Raton, FL:
Per Brinch Hansen. 1994 (June). Multiple‐ CRC Press.
length division revisited: A tour of the Luca Cardelli, James Donahue, Lucille Glass‐
minefield. Software—Practice & Experience, man, Mick Jordan, Bill Kalsow, and Greg
24(6):579–601. Nelson. 1992 (August). Modula‐3 language
definition. SIGPLAN Notices, 27(8):15–42.
Craig Chambers. 1992 (March). The De ics with the Nuprl Proof Development System.
sign and Implementation of the SELF Com Prentice Hall.
piler, an Optimizing Compiler for Object William R. Cook. 2009 (October). On under‐
Oriented Programming Languages. PhD standing data abstraction, revisited. OOP
thesis, Stanford University, Stanford, Cal‐ SLA 2009 Conference Proceedings, in SIG
ifornia. Tech Report STAN‐CS‐92‐1420. PLAN Notices, 44(10):557–572.
Craig Chambers and David Ungar. 1989 Brad J. Cox. 1986. ObjectOriented
(July). Customization: Optimizing com‐ Programming—an Evolutionary Approach.
piler technology for SELF, a dynamically‐ Reading, MA: Addison‐Wesley.
typed object‐oriented programming lan‐ Marcus Crestani and Michael Sperber. 2010 Bibliography
guage. Proceedings of the ACM SIGPLAN ’89 (September). Experience Report: Grow‐
Conference on Programming Language De ing programming languages for begin‐ 735
sign and Implementation, in SIGPLAN No ning students. Proceedings of the Fifteenth
tices, 24(7):146–160. ACM SIGPLAN International Conference on
C. J. Cheney. 1970. A nonrecursive list com‐ Functional Programming (ICFP’10), in SIG
pacting algorithm. Communications of the PLAN Notices, 45(9):229–234.
ACM, 13(11):677–78. Douglas Crockford. 2008. JavaScript: The
Koen Claessen and John Hughes. 2000 Good Parts. O’Reilly.
(September). QuickCheck: A lightweight Ole‐Johan Dahl, Edsger W. Dijkstra, and
tool for random testing of Haskell pro‐ C. A. R. Hoare. 1972. Structured Program
grams. Proceedings of the Fifth ACM SIG ming. London and New York: Academic
PLAN International Conference on Func Press.
tional Programming (ICFP’00), in SIGPLAN Ole‐Johan Dahl and C. A. R. Hoare. 1972. Hi‐
Notices, 35(9):268–279. erarchical program structures. In Struc
William Clinger, Anne H. Hartheimer, and tured Programming, chapter 3, pages 175–
Eric M. Ost. 1999. Implementation strate‐ 220. London and New York: Academic
gies for first‐class continuations. Higher Press.
Order and Symbolic Computation, 12(1):7– Luis Damas and Robin Milner. 1982. Prin‐
45. cipal type‐schemes for functional pro‐
William D. Clinger. 1998 (May). Proper tail grams. In Conference Record of the 9th An
recursion and space efficiency. Proceed nual ACM Symposium on Principles of Pro
ings of the ACM SIGPLAN ’98 Conference on gramming Languages, pages 207–212.
Programming Language Design and Imple Olivier Danvy. 2006 (October). An Analytical
mentation, in SIGPLAN Notices, 33(5):174– Approach to Programs as Data Objects. DSc
185. thesis, BRICS Research Series, University
W. F. Clocksin and C. S. Mellish. 2013. Pro of Aarhus, Aarhus, Denmark.
gramming in Prolog, 5th ed. Springer. Olivier Danvy and Andrzej Filinski. 1990.
Jacques Cohen. 1988 (January). A view of the Abstracting control. In Proceedings of the
origins and development of Prolog. Com 1990 ACM Conference on LISP and Func
munications of the ACM, 31(1):26–36. tional Programming, LFP ’90, pages 151–
A. Colmerauer, H. Kanoui, R. Pasero, and 160.
P. Roussel. 1973 (November). Un systeme Nachum Dershowitz and Edward M Rein‐
de communication homme‐machine gold. 1990. Calendrical calculations.
en Français. Technical Report, Groupe Software—Practice & Experience, 20(9):899–
d’Intelligence Artificielle, Université 928.
d’Aix‐Marseille II. Nachum Dershowitz and Edward M Rein‐
Thomas J. Conroy and Eduardo Pelegri‐ gold. 2018. Calendrical Calculations, 4th
Llopart. 1982. An assessment of ed. Cambridge: Cambridge University
method‐lookup caches for Smalltalk‐80 Press.
implementations. In Smalltalk80: Bits L. Peter Deutsch and Daniel G. Bobrow. 1976
of History, Words of Advice, chapter 13, (September). An efficient, incremental,
pages 239–247. Reading, MA: Addison‐ automatic garbage collector. Communica
Wesley. tions of the ACM, 19(9):522–526.
R. L. Constable, S. F. Allen, H. M. Brom‐ Peter Deutsch and Alan M. Schiffman. 1984
ley, W. R. Cleaveland, J. F. Cremer, R. W. (January). Efficient implementation of
Harper, D. J. Howe, T. B. Knoblock, N. P. the Smalltalk‐80 system. In Conference
Mendler, P. Panangaden, J. T. Sasaki, and Record of the 11th Annual ACM Symposium
S. F. Smith. 1985. Implementing Mathemat on Principles of Programming Languages,
pages 297–302. Matthias Felleisen, Robert Bruce Findler,
Stephan Diehl, Pieter H. Hartel, and Peter and Matthew Flatt. 2009. Semantics En
Sestoft. 2000 (May). Abstract machines for gineering with PLT Redex. MIT Press.
programming language implementation. Matthias Felleisen, Robert Bruce Findler,
Future Generation Computer Systems, 2000 Matthew Flatt, and Shriram Krishna‐
(nr. 7):739–751. murthi. 2018. How to Design Programs: An
Edsger W. Dijkstra. 1968 (March). Letters Introduction to Programming and Comput
to the editor: Go to statement considered ing, 2nd ed. Cambridge, MA: MIT Press.
harmful. Communications of the ACM, 11 Matthias Felleisen and Daniel P. Friedman.
Bibliography (3):147–148. 1997 (December). The Little MLer. Cam‐
Edsger W. Dijkstra. 1976. A Discipline of Pro bridge, MA: MIT Press.
736 gramming. Englewood Cliffs, NJ: Prentice Robert R. Fenichel and Jerome C. Yochelson.
Hall. 1969. A LISP garbage‐collector for virtual‐
Edsger W. Dijkstra, Leslie Lamport, A. J. memory computer systems. Communica
Martin, C. S. Scholten, and E. F. M. Stef‐ tions of the ACM, 12(11):611–12.
fens. 1978 (November). On‐the‐fly gar‐ Andrzej Filinski. 1994. Representing mon‐
bage collection: An exercise in coopera‐ ads. In Conference Record of the 21st Annual
tion. Communications of the ACM, 21(11): ACM Symposium on Principles of Program
966–975. ming Languages, POPL ’94, pages 446–457.
S. Drew, K. John Gough, and J. Leder‐ Kathi Fisler. 2014. The recurring rain‐
mann. 1995. Implementing zero over‐ fall problem. In Proceedings of the
head exception handling. Technical Re‐ Tenth Annual Conference on International
port 95‐12, Faculty of Information Tech‐ Computing Education Research, ICER ’14,
nology, Queensland University of Tech‐ pages 35–42.
nology, Brisbane, Australia. David Flanagan and Yukihiro Matsumoto.
Derek Dreyer, Karl Crary, and Robert 2008. The Ruby Programming Language.
Harper. 2003 (January). A type system for O’Reilly.
higher‐order modules. Conference Record Matthew Flatt. 2012 (January). Creating lan‐
of the 30th Annual ACM Symposium on guages in racket. Communications of the
Principles of Programming Languages, in ACM, 55(1):48–56.
SIGPLAN Notices, 38(1):236–249. Matthew Flatt. 2016 (January). Bindings as
Derek Dreyer, Robert Harper, Manuel M. T. sets of scopes. Conference Record of the 43rd
Chakravarty, and Gabriele Keller. 2007. Annual ACM Symposium on Principles of
Modular type classes. In Proceedings of the Programming Languages, in SIGPLAN No
34th Annual ACM Symposium on Principles tices, 51(1):705–717.
of Programming Languages, pages 63–70. Matthew Flatt, Ryan Culpepper, David
Stéhane Ducasse, Dmitri Zagidulin, Nicolai Darais, and Robert Bruce Findler. 2012.
Hess, and Dimitris Chloupis. 2016. Pharo Macros that work together. Journal of
by Example 5. Square Bracket Associates. Functional Programming, 22:181–216.
Available under a Creative Commons li‐ Matthew Flatt and Matthias Felleisen. 1998
cense. (May). Units: Cool modules for HOT lan‐
R. Kent Dybvig. 1987. The SCHEME Program guages. Proceedings of the ACM SIGPLAN
ming Language. Upper Saddle River, NJ: ’98 Conference on Programming Language
Prentice Hall. Design and Implementation, in SIGPLAN
R. Kent Dybvig, Robert Hieb, and Carl Notices, 33(5):236–248.
Bruggeman. 1992 (December). Syntactic Christopher W. Fraser and David R. Hanson.
abstraction in Scheme. Lisp and Symbolic 1995. A Retargetable C Compiler: Design
Computation, 5(4):295–326. and Implementation. Redwood City, CA:
Sebastian Egner. 2002 (June). Notation Benjamin/Cummings.
for specializing parameters without cur‐ Daniel P. Friedman and Matthias Felleisen.
rying. SRFI 26, in the Scheme Requests 1996. The Little Schemer, 4th ed. MIT Press.
for Implementation series. Daniel P. Friedman, Christopher T. Haynes,
Matthias Felleisen. 1988. The theory and and Eugene E. Kohlbecker. 1984. Pro‐
practice of first‐class prompts. In Confer gramming with continuations. In P. Pep‐
ence Record of the 15th Annual ACM Sym per, editor, Program Transformation and
posium on Principles of Programming Lan Programming Environments, pages 263–
guages, pages 180–190. 274. Springer‐Verlag.
Emden Gansner and John Reppy, editors. der Creative Commons. Best sought from
2002. The Standard ML Basis Library. New Harper’s home page at Carnegie Mellon
York: Cambridge University Press. University.
Martin Gasbichler and Michael Sperber. Robert Harper and Mark Lillibridge. 1994
2002. Final shift for call/cc: Direct im‐ (January). A type‐theoretic approach to
plementation of shift and reset. In Pro higher‐order modules with sharing. In
ceedings of the Seventh ACM SIGPLAN Inter Conference Record of the 21st Annual ACM
national Conference on Functional Program Symposium on Principles of Programming
ming (ICFP’02), pages 271–282. Languages, pages 123–137.
Jeremy Gibbons and Geraint Jones. 1998 Robert Harper and Christopher Stone. 2000. Bibliography
(September). The under‐appreciated un‐ A type‐theoretic interpretation of Stan‐
fold. Proceedings of the 1998 ACM SIGPLAN dard ML. In Gordon Plotkin, Colin Stir‐ 737
International Conference on Functional Pro ling, and Mads Tofte, editors, Proof, Lan
gramming, in SIGPLAN Notices, 34(1):273– guage and Interaction: Essays in Honour of
279. Robin Milner. Cambridge, MA: MIT Press.
A. Goldberg. 1983. Smalltalk80: The Interac Robert W. Harper. 2012. Practical Founda
tive Programming Environment. Reading, tions for Programming Languages. Cam‐
MA: Addison‐Wesley. bridge: Cambridge University Press.
Adele Goldberg and David Robson. 1983. Brian Harvey and Matthew Wright. 1994.
Smalltalk80: The Language and Its Imple Simply Scheme: Introducing Computer Sci
mentation. Reading, MA: Addison‐Wesley. ence. Cambridge, MA: MIT Press.
James Gosling, Bill Joy, and Guy Steele. 1997. Christopher T. Haynes, Daniel P. Friedman,
The Java Language Specification. The Java and Mitchell Wand. 1984. Continuations
Series. Reading, MA: Addison‐Wesley. and coroutines. In Proceedings of the 1984
Paul Graham. 1993. On Lisp: Advanced Tech ACM Symposium on LISP and Functional
niques for Common Lisp. Upper Saddle Programming, pages 293–298.
River, NJ: Prentice Hall. May be down‐ Greg Hendershott. 2020. Fear of macros.
loadable from http://www.paulgraham. URL http://www.greghendershott.com/
com/onlisp.html. fear‑of‑macros/. Tutorial from the au‐
Justin O. Graver and Ralph E. Johnson. 1990. thor’s web site.
A type system for Smalltalk. In Confer Peter Henderson. 1980. Functional Program
ence Record of the 17th Annual ACM Sym ming: Application and Implementation. En‐
posium on Principles of Programming Lan glewood Cliffs, NJ: Prentice Hall.
guages, pages 136–150. Matthew Hertz and Emery D. Berger. 2005
David Gries. 1981. The Science of Program (October). Quantifying the performance
ming. Springer‐Verlag. of garbage collection vs. explicit memory
Ralph E. Griswold and Madge T. Griswold. management. OOPSLA ’05 Conference Pro
1996. The Icon Programming Language, 3rd ceedings, in SIGPLAN Notices, 40(10):313–
ed. San Jose, CA: Peer‐to‐Peer Communi‐ 326.
cations. Rich Hickey. 2020 (June). A history of Clo‐
Dan Grossman, Michael Hicks, Trevor Jim, jure. Proceedings of the ACM on Program
and Greg Morrisett. 2005 (January). Cy‐ ming Languages, 4(HOPL).
clone: A type‐safe dialect of C. C/C++ Users Robert Hieb, R. Kent Dybvig, and Carl
Journal, 23(1). Bruggeman. 1990 (June). Representing
Dirk Grunwald and Benjamin G. Zorn. 1993 control in the presence of first‐class con‐
(August). CustoMalloc: Efficient syn‐ tinuations. Proceedings of the ACM SIG
thesized memory allocators. Software— PLAN ’90 Conference on Programming Lan
Practice & Experience, 23(8):851–869. guage Design and Implementation, in SIG
David R. Hanson. 1996. C Interfaces and PLAN Notices, 25(6):66–77.
Implementations. Reading, MA: Addison‐ J. Roger Hindley. 1969. The principal type
Wesley. scheme of an object in combinatory logic.
Robert Harper. 1986 (September). Intro‐ Transactions of the American Mathematical
duction to Standard ML. Technical Re‐ Society, 146:29–60.
port ECS–LFCS–86–14, Laboratory for the Ralf Hinze. 2003. Fun with phantom types.
Foundations of Computer Science, Edin‐ In Jeremy Gibbons and Oege de Moor, ed‐
burgh University, Edinburgh. itors, The Fun of Programming, chapter 12,
Robert Harper. 2011. Programming in pages 245–262. London: Palgrave Macmil‐
Standard ML. Book draft licensed un‐ lan.
C. A. R. Hoare. 1972. Proof of correctness of Roberto Ierusalimschy. 2016 (August). Pro
data representations. Acta Informatica, 1: gramming in Lua, 4th ed. Lua.org.
271–281. Roberto Ierusalimschy, Luiz H. de
C. A. R. Hoare. 2009 (August). Null refer‐ Figueiredo, and Waldemar Celes. 2007
ences: The billion dollar mistake. Presen‐ (June). The evolution of Lua. In Proceed
tation delivered at QCon. ings of the Third ACM SIGPLAN Conference
C. A. R. Hoare, I. J. Hayes, He Jifeng, C. C. on History of Programming Languages,
Morgan, A. W. Roscoe, J. W. Sanders, pages 2‐1–2‐26.
I. H. Sørensen, J. M. Spivey, and B. A. Dan Ingalls, Ted Kaehler, John Maloney,
Bibliography Sufrin. 1987 (August). Laws of program‐ Scott Wallace, and Alan Kay. 1997 (Oc‐
ming. Communications of the ACM, 30(8): tober). Back to the future: The story of
738 672–686. Corrected in September 1987. Squeak—a practical Smalltalk written in
C. J. Hogger. 1984. Introduction to Logic Pro itself. OOPSLA ’97 Conference Proceedings,
gramming. London: Academic Press. in SIGPLAN Notices, 32(10):318–326.
John E. Hopcroft and Jeffrey D. Ullman. Daniel Ingalls. 2020 (June). The evolution
1979. Introduction to Automata Theory, of Smalltalk: From Smalltalk‐72 through
Languages and Computation. Reading, Squeak. Proceedings of the ACM on Pro
MA: Addison‐Wesley. gramming Languages, 4(HOPL).
Jim Horning, Bill Kalsow, Paul McJones, and Kenneth E. Iverson. 1962. A Programming
Greg Nelson. 1993 (December). Some Language. New York: John Wiley & Sons.
useful Modula‐3 interfaces. Research Re‐ Ralph E. Johnson, Justin O. Graver, and
port 113, Digital Systems Research Cen‐ Lawrence W. Zurawski. 1988. TS: An
ter, Palo Alto, CA. optimizing compiler for Smalltalk. In
Paul Hudak, John Hughes, Simon L. Peyton Norman Meyrowitz, editor, OOPSLA’88:
Jones, and Philip Wadler. 2007. A his‐ ObjectOriented Programming Systems, Lan
tory of Haskell: Being lazy with class. In guages and Applications: Conference Pro
Barbara G. Ryder and Brent Hailpern, edi‐ ceedings, pages 18–26.
tors, Proceedings of the Third ACM SIGPLAN Mark S. Johnstone and Paul R. Wilson. 1998
History of Programming Languages Confer (October). The memory fragmentation
ence (HOPLIII), pages 1–55. problem: Solved? Proceedings of the First
John Hughes. 1989 (April). Why functional International Symposium on Memory Man
programming matters. The Computer Jour agement, in SIGPLAN Notices, 34(3):26–36.
nal, 32(2):98–107. Mark P. Jones. 1993 (November). Coher‐
John Hughes. 1995. The design of a ence for qualified types. Technical Report
pretty‐printing library. In Johan Jeuring YALEU/DCS/RR‐989, Yale University, New
and Erik Meijer, editors, Advanced Func Haven, CT.
tional Programming, LNCS, volume 925, Mark P. Jones. 1999 (October). Typing
pages 53–96. Springer Verlag. Haskell in Haskell. In Proceedings of
John Hughes. 2016. Experiences with the 1999 Haskell Workshop. Published in
QuickCheck: Testing the hard stuff and Technical Report UU‐CS‐1999‐28, Depart‐
staying sane. In A List of Successes That ment of Computer Science, University of
Can Change the World—Essays Dedicated to Utrecht. Additional resources at http://
Philip Wadler on the Occasion of His 60th www.cse.ogi.edu/~mpj/thih.
Birthday, pages 169–186. Springer. Richard Jones, Antony Hosking, and Eliot
Galen C. Hunt and James R. Larus. 2007 Moss. 2011. The Garbage Collection Hand
(April). Singularity: Rethinking the soft‐ book: The Art of Automatic Memory Man
ware stack. SIGOPS Operating Systems Re agement. Chapman & Hall/CRC.
view, 41(2):37–49. Richard Jones and Rafael Lins. 1996. Gar
Graham Hutton and Erik Meijer. 1996 bage Collection: Algorithms for Automatic
(January). Monadic parser combinators. Dynamic Memory Management. New York:
Technical Report NOTTCS‐TR‐96‐4, Uni‐ Wiley. Reprinted in 1999 with improved
versity of Nottingham. index and corrected errata.
Jean D. Ichbiah, Bernd Krieg‐Brueckner, Ted Kaehler and Dave Patterson. 1986. A
Brian A. Wichmann, John G. P. Barnes, Taste of Smalltalk. New York: W. W. Nor‐
Olivier Roubine, and Jean‐Claude He‐ ton and Co.
liard. 1979 (June). Rationale for the design Gilles Kahn. 1987. Natural semantics. In
of the Ada programming language. SIG Proceedings of the Symposium on Theoretical
PLAN Notices, 14(6b):1–261. Aspects of Computer Science (STACS), LNCS,
volume 247, pages 22–39. Springer‐Verlag. munications of the ACM, CACM, 31(1):38–
Samuel N. Kamin. 1990. Programming 43.
Languages: An InterpreterBased Approach. Robert A. Kowalski. 2014. Logic for Problem
Reading, MA: Addison‐Wesley. Solving, Revisited. Books on Demand.
A. C. Kay. 1993 (March). The early history of Glenn Krasner, editor. 1983. Smalltalk80:
Smalltalk. SIGPLAN Notices, 28(3):69–95. Bits of History, Words of Advice. Boston:
Richard Kelsey, William Clinger, and Addison‐Wesley Longman.
Jonathan Rees. 1998 (September). P. J. Landin. 1966. The next 700 program‐
Revised5 report on the algorithmic lan‐ ming languages. Communications of the
guage Scheme. SIGPLAN Notices, 33(9): ACM, 9(3):157–166. Bibliography
26–76. Peter J. Landin. 1964 (January). The mechan‐
Brian W. Kernighan and Dennis M. Ritchie. ical evaluation of expressions. Computer 739
1988. The C Programming Language, 2nd Journal, 6(4):308–320.
ed. Englewood Cliffs, NJ: Prentice Hall. Konstantin Läufer and Martin Odersky. 1994
Oleg Kiselyov. 2012 (August). An (September). Polymorphic type inference
argument against call/cc. URL and abstract data types. ACM Transactions
http://okmij.org/ftp/continuations/ on Programming Languages and Systems, 16
against‑callcc.html. Referenced in (5):1411–1430.
August 2015. Xavier Leroy. 1994 (January). Manifest types,
Donald E. Knuth. 1965 (December). On the modules, and separate compilation. In
translation of languages from left to right. Conference Record of the 21st Annual ACM
Information and Control, 8(6):607–639. Symposium on Principles of Programming
Donald E. Knuth. 1973. Fundamental Al Languages, pages 109–122.
gorithms, 2nd ed., volume 1 of The Art Xavier Leroy. 1999 (September). Ob‐
of Computer Programming. Reading, MA: jects and classes vs. modules in Objective
Addison‐Wesley. Caml. URL http://pauillac.inria.fr/
Donald E. Knuth. 1974 (December). Struc‐ ~xleroy/talks/icfp99.ps.gz. Invited
tured programming with go to state‐ talk delivered at the 1999 International
ments. ACM Computing Surveys, 6(4):261– Conference on Functional Programming
301. (ICFP).
Donald E. Knuth. 1981. Seminumerical Al Xavier Leroy. 2000 (May). A modular mod‐
gorithms, 2nd ed., volume 2 of The Art ule system. Journal of Functional Program
of Computer Programming. Reading, MA: ming, 10(3):269–303.
Addison‐Wesley. Henry Lieberman and Carl Hewitt. 1983
Donald E. Knuth. 1984. Literate program‐ (June). A real‐time garbage collector
ming. The Computer Journal, 27(2):97–111. based on the lifetimes of objects. Commu
Andrew Koenig. 1994. An anecdote about nications of the ACM, 26(6):419–429.
ML type inference. In Proceedings of the Miran Lipovača. 2011. Learn You a Haskell for
USENIX 1994 Very High Level Languages Great Good!: A Beginner’s Guide. San Fran‐
Symposium, VHLLS’94, page 1. Berkeley, cisco: No Starch Press.
CA: USENIX Association. Barbara Liskov. 1996. A history of CLU. In
Peter M. Kogge. 1990. The Architecture of Sym Thomas J. Bergin, Jr. and Richard G. Gib‐
bolic Computers. New York: McGraw‐Hill. son, Jr., editors, History of Programming
Eugene Kohlbecker, Daniel P. Friedman, languages—II, pages 471–510. New York:
Matthias Felleisen, and Bruce Duba. 1986. ACM.
Hygienic macro expansion. In Pro Barbara Liskov and John Guttag. 1986.
ceedings of the 1986 ACM Conference on Abstraction and Specification in Program
LISP and Functional Programming, LFP ’86, Development. Cambridge, MA: MIT
pages 151–161. Press/McGraw‐Hill.
Robert A. Kowalski. 1974. Predicate logic Barbara Liskov, Alan Snyder, Russell Atkin‐
as a programming language. Proc. IFIP 4, son, and Craig Schaffert. 1977 (August).
pages 569–574. Abstraction mechanisms in CLU. Commu
Robert A. Kowalski. 1979. Logic for Problem nications of the ACM, 20(8):564–576. Re‐
Solving. New York: North Holland. As of published in Readings in ObjectOriented
2022, available from Kowalski’s site at Im‐ Database Systems, S. Zdonik and D. Maier,
perial College London. Morgan Kaufman, 1990.
Robert A. Kowalski. 1988 (January). The Barbara Liskov and Stephen Zilles. 1974
early years of logic programming. Com (March). Programming with abstract data
types. Proceedings of the ACM SIGPLAN MA: Addison‐Wesley.
Symposium on Very High Level Languages, Bertrand Meyer. 1992. Eiffel: The Language.
in SIGPLAN Notices, 9(4):50–59. London: Prentice Hall International.
Barbara H. Liskov and Alan Snyder. 1979 Bertrand Meyer. 1997. ObjectOriented Soft
(November). Exception handling in CLU. ware Construction, 2nd ed. Englewood
IEEE Transactions on Software Engineering, Cliffs, NJ: Prentice‐Hall.
SE‐5(6):546–558. Todd Millstein, Colin Bleckner, and Craig
Barbara H. Liskov and Jeannette M. Wing. Chambers. 2004. Modular typechecking
1994 (November). A behavioral notion of for hierarchically extensible datatypes
Bibliography subtyping. ACM Transactions on Program and functions. ACM Transactions on Pro
ming Languages and Systems, 16(6):1811– gramming Languages and Systems, 26(5):
740 1841. 836–889.
Chi‐Keung Luk, Robert Cohn, Robert Robin Milner. 1978 (December). A theory
Muth, Harish Patil, Artur Klauser, Geoff of type polymorphism in programming.
Lowney, Steven Wallace, Vijay Janapa Journal of Computer and System Sciences,
Reddi, and Kim Hazelwood. 2005 (June). 17:348–375.
Pin: Building customized program anal‐ Robin Milner. 1983. How ML evolved.
ysis tools with dynamic instrumentation. Polymorphism—The ML/LCF/Hope Newslet
Proceedings of the ACM SIGPLAN ’05 Con ter, 1(1).
ference on Programming Language Design Robin Milner. 1999 (May). Communicating
and Implementation, in SIGPLAN Notices, and Mobile Systems: The π Calculus. Cam‐
40(6):190–200. bridge: Cambridge University Press.
Luc Maranget. 2007. Warnings for pattern Robin Milner and Mads Tofte. 1991. Com
matching. Journal of Functional Program mentary on Standard ML. Cambridge, MA:
ming, 17(3):387–421. MIT Press.
Luc Maranget. 2008. Compiling pattern Robin Milner, Mads Tofte, Robert Harper,
matching to good decision trees. In Pro and David MacQueen. 1997. The Defini
ceedings of the 2008 ACM SIGPLAN Work tion of Standard ML (Revised). Cambridge,
shop on ML, pages 35–46. MA: MIT Press.
Simon Marlow, Tim Harris, Roshan P. Marvin L. Minsky. 1963 (December). A Lisp
James, and Simon Peyton Jones. 2008 garbage collector algorithm using serial
(June). Parallel generational‐copying gar‐ secondary storage. Technical Report
bage collection with a block‐structured Memo 58, Project MAC, MIT, Cambridge,
heap. In ISMM ’08: Proceedings of the 7th MA.
International Symposium on Memory Man John C. Mitchell and Gordon D. Plotkin. 1988
agement. (July). Abstract types have existential
Moe Masuko and Kenichi Asai. 2009. Di‐ type. ACM Transactions on Programming
rect implementation of shift and reset in Languages and Systems, 10(3):470–502.
the MinCaml compiler. In Proceedings of Calvin N. Mooers. 1966 (March). TRAC, a
the 2009 ACM SIGPLAN Workshop on ML, procedure‐describing language for the re‐
pages 49–60. active typewriter. Communications of the
Conor McBride. 2004. Epigram: Practical ACM, 9(3):215–219.
programming with dependent types. In David A. Moon. 1986 (October). Object‐
International School on Advanced Func oriented programming with flavours.
tional Programming, pages 130–170. In Proceedings of the ACM Conference on
Springer. ObjectOriented Programming Systems,
Conor McBride and Ross Paterson. 2008 Languages, and Applications, pages 1–8.
(January). Applicative programming with George C. Necula, Scott McPeak, and West‐
effects. Journal of Functional Program ley Weimer. 2002 (January). CCured:
ming, 18(1):1–13. Type‐safe retrofitting of legacy code. Con
John McCarthy. 1960 (April). Recursive func‐ ference Record of the 29th Annual ACM Sym
tions of symbolic expressions and their posium on Principles of Programming Lan
computation by machine, part I. Commu guages, in SIGPLAN Notices, 37(1):128–139.
nications of the ACM, 3(4):184–195. Greg Nelson, editor. 1991. Systems Program
John McCarthy. 1962. Lisp 1.5 Programmer’s ming with Modula3. Englewood Cliffs, NJ:
Manual. Cambridge, MA: MIT Press. Prentice Hall.
Sandi Metz. 2013. Practical ObjectOriented Nicholas Nethercote and Julian Seward.
Design in Ruby: An Agile Primer. Reading, 2007a. How to shadow every byte of
memory used by a program. In Proceed Marco Gaboardi, Michael Greenberg,
ings of the 3rd International Conference on Cǎtǎlin Hriţcu, Vilhelm Sjöberg, and
Virtual Execution Environments, VEE ’07, Brent Yorgey. 2016 (May). Soft‐
pages 65–74. ware foundations. URL https:
Nicholas Nethercote and Julian Seward. //www.cis.upenn.edu/~bcpierce/sf/.
2007b. Valgrind: A framework for heavy‐ Version 4.0.
weight dynamic binary instrumentation. Rob Pike. 1990 (July). The implementation
Proceedings of the ACM SIGPLAN ’07 Con of Newsqueak. Software—Practice & Expe
ference on Programming Language Design rience, 20(7):649–659.
and Implementation, in SIGPLAN Notices, Gordon D. Plotkin. 1981 (September). A Bibliography
42(6):89–100. structural approach to operational se‐
Nils J. Nilsson. 1980. Principles of Artificial mantics. Technical Report DAIMI FN‐19, 741
Intelligence. Tioga/Morgan Kaufman. Department of Computer Science, Aarhus
James Noble, Andrew P. Black, Kim B. University, Aarhus, Denmark.
Bruce, Michael Homer, and Mark S. François Pottier and Didier Rémy. 2005. The
Miller. 2016. The left hand of equals. In essence of ML type inference. In Ben‐
Proceedings of the 2016 ACM International jamin C. Pierce, editor, Advanced Topics in
Symposium on New Ideas, New Paradigms, Types and Programming Languages, chap‐
and Reflections on Programming and Soft ter 10, pages 389–489. Cambridge, MA:
ware, Onward! 2016, pages 224–237. MIT Press.
Kristen Nygaard and Ole‐Johan Dahl. 1981. François Pottier and Yann Régis‐Gianas.
The development of the SIMULA lan‐ 2006 (March). Towards efficient, typed
guages. In Richard L. Wexelblat, edi‐ LR parsers. In ACM Workshop on ML,
tor, History of Programming Languages I, pages 155–180.
pages 439–480. New York: ACM. Todd A. Proebsting. 1997 (June). Sim‐
Martin Odersky, Lex Spoon, and Bill Ven‐ ple translation of goal‐directed evalua‐
ners. 2019. Programming in Scala, 4th ed. tion. Proceedings of the ACM SIGPLAN ’97
Walnut Creek, CA: Artima Press. Conference on Programming Language De
Martin Odersky, Martin Sulzmann, and Mar‐ sign and Implementation, in SIGPLAN No
tin Wehr. 1999. Type inference with con‐ tices, 32(5):1–6.
strained types. Theory and Practice of Ob Norman Ramsey. 1990 (April). Concurrent
ject Systems, 5(1):35–55. programming in ML. Technical Report
Melissa E. O’Neill. 2009 (January). The TR‐262‐90, Department of Computer Sci‐
genuine sieve of Eratosthenes. Journal of ence, Princeton University.
Functional Programming, 19(1):95–106. Norman Ramsey. 1994 (September). Literate
Derek C. Oppen. 1980 (October). Prettyprint‐ programming simplified. IEEE Software,
ing. ACM Transactions on Programming 11(5):97–105.
Languages and Systems, 2(4):465–483. Norman Ramsey. 1999. Eliminating spu‐
David Lorge Parnas. 1972 (December). On rious error messages using exceptions,
the criteria for decomposing systems into polymorphism, and higher‐order func‐
modules. Communications of the ACM, 15 tions. Computer Journal, 42(5):360–372.
(12):1053–1058. Norman Ramsey. 2005 (September). ML
Laurence C. Paulson. 1996. ML for the Work module mania: A type‐safe, separately
ing Programmer, 2nd ed. New York: Cam‐ compiled, extensible interpreter. In ACM
bridge University Press. SIGPLAN Workshop on ML, pages 172–202.
Nigel Perry. 1991. The Implementation of Prac Norman Ramsey, João Dias, and Simon L.
tical Functional Programming Languages. Peyton Jones. 2010. Hoopl: A modu‐
PhD thesis, Imperial College, London. lar, reusable library for dataflow analy‐
Simon Peyton Jones, Dimitrios Vytin‐ sis and transformation. Proceedings of the
iotis, Stephanie Weirich, and Mark 3rd ACM SIGPLAN Symposium on Haskell
Shields. 2007. Practical type inference for (Haskell 2010), in SIGPLAN Notices, 45(11):
arbitrary‐rank types. Journal of Functional 121–134.
Programming, 17(1):1–82. Norman Ramsey, Kathleen Fisher, and Paul
Benjamin C. Pierce. 2002. Types and Program Govereau. 2005 (September). An ex‐
ming Languages. Cambridge, MA: MIT pressive language of signatures. In Pro
Press. ceedings of the Tenth ACM SIGPLAN Inter
Benjamin C. Pierce, Arthur Azevedo national Conference on Functional Program
de Amorim, Chris Casinghino, ming (ICFP’05), pages 27–40.
Jonathan Rees and William Clinger. 1986 Tom Schrijvers, Simon Peyton Jones, Martin
(December). Revised3 report on the algo‐ Sulzmann, and Dimitrios Vytiniotis. 2009
rithmic language Scheme. SIGPLAN No (August). Complete and decidable type in‐
tices, 21(12):37–79. ference for GADTs. Proceedings of the Four
E. Reingold and R. Reingold. 1988. PascAl teenth ACM SIGPLAN International Confer
gorithms: An Introduction to Programming. ence on Functional Programming (ICFP’09),
Scott, Foresman. in SIGPLAN Notices, 44(9):341–352.
John Reynolds. 1972 (August). Defini‐ Kevin Scott and Norman Ramsey. 2000
tional interpreters for higher‐order pro‐ (May). When do match‐compilation
Bibliography gramming languages. In Proceedings of the heuristics matter? Technical Report
25th ACM National Conference, pages 717– CS‐2000‐13, Department of Computer Sci‐
742 740. Reprinted in HigherOrder and Sym ence, University of Virginia.
bolic Computation, 11(4):363–397, 1998. Robert Sedgewick. 1988. Algorithms, 2nd ed.
John C. Reynolds. 1974. Towards a theory of Addison‐Wesley.
type structure. In Colloque sur la Program Manuel Serrano and Hans‐Juergen Boehm.
mation, Paris, France, LNCS, volume 19, 2000 (September). Understanding mem‐
pages 408–425. Springer‐Verlag. ory allocation of Scheme programs. Pro
John C. Reynolds. 1978. User‐defined types ceedings of the Fifth ACM SIGPLAN Inter
and procedural data structures as comple‐ national Conference on Functional Program
mentary approaches to data abstraction. ming (ICFP’00), in SIGPLAN Notices, 35(9):
In David Gries, editor, Programming Meth 245–256.
odology, Texts and Monographs in Com‐ Alex Shinn, John Cowan, and Arthur A.
puter Science, pages 309–317. New York: Gleckler. 2013. Revised7 Report on the
Springer. algorithmic language Scheme. Technical
John C. Reynolds. 1993 (November). The dis‐ Report, R7RS Working Group 1. URL
coveries of continuations. Lisp and Sym https://small.r7rs.org/attachment/
bolic Computation, 6(3/4):233–248. r7rs.pdf.
John C. Reynolds. 1998 (December). Def‐ Justin Slepak, Olin Shivers, and Panagiotis
initional interpreters for higher‐order Manolios. 2014. An array‐oriented lan‐
programming languages. HigherOrder guage with static rank polymorphism. In
and Symbolic Computation, 11(4):363–397. 23rd European Symposium on Programming
Reprinted from the proceedings of the (ESOP 2014), pages 27–46.
25th ACM National Conference (1972). Frederick Smith and Greg Morrisett. 1999
E. S. Roberts. 1986. Thinking Recursively. (March). Comparing mostly‐copying and
New York: John Wiley & Sons. mark‐sweep conservative collection. Pro
Eric S. Roberts. 1989 (March). Implement‐ ceedings of the First International Sympo
ing exceptions in C. Technical Report sium on Memory Management (ISMM’98),
Research Report 40, Digital Systems Re‐ in SIGPLAN Notices, 34(3):68–78.
search Center, Palo Alto, CA. Elliott Soloway. 1986 (September). Learning
J. Alan Robinson. 1965 (January). A machine‐ to program = learning to construct mecha‐
oriented logic based on resolution princi‐ nisms and explanations. Communications
ple. Journal of the ACM, 12(1):23–49. of the ACM, 29(9):850–858.
J. Alan Robinson. 1983. Logic Michael Sperber, R. Kent Dybvig, Matthew
programming—past, present and fu‐ Flatt, Anton van Straaten, Robby Findler,
ture. New Generation Comput. (Japan), 1 and Jacob Matthews. 2009. Revised6 Re‐
(2):107–124. QA 76 N 48. port on the algorithmic language Scheme.
J. S. Rohl. 1984. Recursion via Pascal. Cam‐ Journal of Functional Programming, 19
bridge: Cambridge University Press. (Supplement S1):1–301.
Raúl Rojas. 2015. A tutorial introduction to Mike Spivey. 1990 (June). A functional the‐
the lambda calculus. http://arxiv.org/ ory of exceptions. Science of Computer Pro
abs/1503.09060. gramming, 14(1):25–42.
Colin Runciman and David Wakeling. 1993 Raymie Stata and John V. Guttag. 1995 (Octo‐
(April). Heap profiling of lazy functional ber). Modular reasoning in the presence
programs. Journal of Functional Program of subclassing. OOPSLA ’95 Conference Pro
ming, 3(2):217–246. ceedings, in SIGPLAN Notices, 30(10):200–
David A. Schmidt. 1986. Denotational Seman 214.
tics: A Methodology for Language Develop Raymond Paul Stata. 1996. Modularity in the
ment. Reading, MA: Allyn and Bacon. Presence of Subclassing. PhD thesis, Mas‐
sachusetts Institute of Technology. David Ungar. 1984 (May). Generation
Guy Lewis Steele, Jr. 1977 (October). De‐ scavenging: A non‐disruptive high per‐
bunking the “expensive procedure call” formance storage reclamation algorithm.
myth or, procedure call implementations Proceedings of the ACM SIGSOFT/SIGPLAN
considered harmful or, LAMBDA: The ul‐ Symposium on Practical Software Develop
timate GOTO. In Proceedings ACM Annual ment Environments, in SIGPLAN Notices, 19
Conference, pages 153–162. (5):157–167.
Guy Lewis Steele, Jr. 1984. Common Lisp: The David Ungar and Randall B. Smith. 1987.
Language. Digital Press. Self: The power of simplicity. OOPSLA
Guy Lewis Steele, Jr. 2017. It’s time for a ’87 Conference Proceedings, in SIGPLAN No Bibliography
new old language. In Proceedings of the tices, pages 227–242.
22nd ACM SIGPLAN Symposium on Princi Peter Van der Linden. 1994. Expert C Pro 743
ples and Practice of Parallel Programming, gramming: Deep C Secrets. Englewood
PPoPP ’17, page 1. A recording of Steele’s Cliffs, NJ: Prentice Hall Professional.
talk can likely be found on the Web. Guido van Rossum. 1998. A tour of the
Guy Lewis Steele, Jr. and Gerald Jay Suss‐ Python language. In R. Ege, M. Singh, and
man. 1976 (March). Lambda: The ulti‐ B. Meyer, editors, Proceedings. Technology
mate imperative. Technical Report AIM‐ of ObjectOriented Languages and Systems,
353, Massachusetts Institute of Technol‐ TOOLS23, page 370. Silver Spring, MD:
ogy. IEEE Computer Society.
Guy Lewis Steele, Jr. and Gerald Jay Suss‐ David Vengerov. 2009. Modeling, analysis
man. 1978 (May). The art of the inter‐ and throughput optimization of a genera‐
preter or, the modularity complex (parts tional garbage collector. In Proceedings of
zero, one, and two). Technical Report the 2009 International Symposium on Mem
AIM‐453, Massachusetts Institute of Tech‐ ory Management (ISMM ’09), pages 1–9.
nology. Dimitrios Vytiniotis, Simon Peyton Jones,
L. Sterling and E. Shapiro. 1986. The Art of and Tom Schrijvers. 2010. Let should
Prolog. Cambridge, MA: MIT Press. not be generalised. In Proceedings of
Joseph E. Stoy. 1977. Denotational Seman the 5th ACM SIGPLAN workshop on Types
tics: The ScottStrachey Approach to Pro in Language Design and Implementation
gramming Language Theory. Cambridge, (TLDI ’10), pages 39–50.
MA: MIT Press. Dimitrios Vytiniotis, Simon Peyton Jones,
Christopher Strachey and Christopher P. Tom Schrijvers, and Martin Sulzmann.
Wadsworth. 2000 (April). Continuations: 2011 (September). OutsideIn(X): Modu‐
A mathematical semantics for handling lar type inference with local assumptions.
full jumps. Higher Order Symbol. Com Journal of Functional Programming, 21(4‐
put., 13(1‐2):135–152. Oxford monograph 5):333–412.
from 1974 republished in a special issue Philip Wadler. 2003. A prettier printer. In
of HOSC devoted to Christopher Strachey. Jeremy Gibbons and Oege de Moor, edi‐
Bjarne Stroustrup. 1997. The C++ Pro tors, The Fun of Programming, chapter 11,
gramming Language, 3rd ed. Reading, MA: pages 223–244. London: Palgrave Macmil‐
Addison‐Wesley. lan.
Gerald Jay Sussman and Guy Lewis Steele, Jr. Philip Wadler. 2015 (November). Propo‐
1975 (December). Scheme: An interpreter sitions as types. Communications of the
for extended lambda calculus. MIT AI ACM, 58(12):75–84.
Memo No. 349, reprinted in HigherOrder Philip Wadler and Stephen Blott. 1989 (Jan‐
and Symbolic Computation 11(4):405–439, uary). How to make adhoc polymorphism
Dec 1998. less ad hoc. In Conference Record of the
Mads Tofte. 2009 (August). Tips for 16th Annual ACM Symposium on Principles
computer scientists on Standard ML (re‐ of Programming Languages, pages 60–76.
vised). Linked from http://www.itu.dk/ Daniel C. Wang, Andrew W. Appel, Jeff L.
people/tofte. Korn, and Christopher S. Serra. 1997 (Oc‐
D. S. Touretzky. 1984. LISP: A Gentle Introduc tober). The Zephyr Abstract Syntax De‐
tion to Symbolic Computation. New York: scription Language. In Proceedings of the
Harper & Row. 2nd USENIX Conference on DomainSpecific
Jeffrey D. Ullman. 1997. Elements of ML Languages, pages 213–227.
Programming, ML97 Edition. Englewood David H. D. Warren. 1983 (October). An
Cliffs, NJ: Prentice Hall. abstract Prolog instruction set. Technical
Report 309, Artificial Intelligence Center, nications of the ACM, 14(4):221–227.
Computer Science and Technology Divi‐ Niklaus Wirth. 1977 (November). What can
sion, SRI International, Menlo Park, CA. we do about the unnecessary diversity of
R. L. Wexelblat, editor. 1981. History of notation for syntactic definitions? Com
Programming Languages. New York: Aca‐ munications of the ACM, 20(11):822–823.
demic Press. Niklaus Wirth. 1982. Programming in
Robert Wilensky. 1986. Common LISPcraft. Modula2. Berlin: Springer.
New York: W. W. Norton. Robert S. Wolf. 2005. A Tour through Math
Paul R. Wilson. 1992 (September). Unipro‐ ematical Logic. Mathematical Association
Bibliography cessor garbage collection techniques. In of America.
Proceedings of the International Workshop Andrew K. Wright. 1995 (December). Simple
744 on Memory Management, volume 637 of imperative polymorphism. Lisp and Sym
LCNS, pages 1–42. Springer‐Verlag. bolic Computation, 8(4):343–355.
Paul R. Wilson, Mark S. Johnstone, Michael William A. Wulf, R. L. London, and Mary
Neely, and David Boles. 1995 (Septem‐ Shaw. 1976 (December). An introduc‐
ber). Dynamic storage allocation: A sur‐ tion to the construction and verification
vey and critical review. In Henry Baker, of Alphard programs. IEEE Transactions
editor, Proceedings of International Work on Software Engineering, 2(4):253–265.
shop on Memory Management, LNCS, vol‐ Hongwei Xi and Frank Pfenning. 1998
ume 986. Springer‐Verlag. (May). Eliminating array bound checking
Terry Winograd. 1972. Understanding Natu through dependent types. Proceedings of
ral Language. New York: Academic Press. the ACM SIGPLAN ’98 Conference on Pro
P. H. Winston and B. K. P. Horn. 1984. Lisp gramming Language Design and Implemen
Second Edition. Reading: Addison‐Wesley. tation, in SIGPLAN Notices, 33(5):249–257.
Patrick H. Winston. 1977. Artificial Intelli Benjamin Zorn. 1993 (July). The measured
gence. Reading, MA: Addison‐Wesley. cost of conservative garbage collection.
Niklaus Wirth. 1971 (April). Program devel‐ Software—Practice & Experience, 23(7):733–
opment by stepwise refinement. Commu 756.
Key words and phrases
745
garbage collection, 289 (Garbage collec‐ message selector, 709 (µSmalltalk)
tion) metalanguage, 318 (µScheme in ML)
general, at least as, 441 (nano‐ML) metatheoretic proof, 69 (Impcore)
generalization, 442 (nano‐ML) metatheory, 69 (Impcore)
generational collection, 289 (Garbage metavariable, 70 (Impcore)
collection) method, 709 (µSmalltalk)
generativity, 501 (µML) method dispatch, 710 (µSmalltalk)
generic module, 586 (Molecule) module, 587 (Molecule)
Key words goal, S96 (µProlog) module type, 587 (Molecule)
and phrases grammar, 69 (Impcore) monomorphic function, 173 (µScheme)
ground term, S96 (µProlog) monomorphic type system, 384 (type
746 systems)
heap, 289 (Garbage collection) monotype, 384 (type systems), 442 (nano‐
heap allocation, 289 (Garbage collection) ML)
heap object, 289 (Garbage collection) most general, 442 (nano‐ML)
higher‐order function, 173 (µScheme) multiple inheritance, 710 (µSmalltalk)
hole, 245 (µScheme+) mutable, 173 (µScheme)
mutable reference cell, 318 (µScheme in
implementation, 586 (Molecule) ML)
information hiding, 586 (Molecule) mutator, 290 (Garbage collection)
inheritance, 709 (µSmalltalk) mutual recursion, 319 (µScheme in ML)
initial basis, 69 (Impcore)
instance, 442 (nano‐ML), 709 (µSmalltalk) natural deduction, 70 (Impcore)
instance variable, 709 (µSmalltalk) nested function, 173 (µScheme)
instantiation, 384 (type systems), number, 710 (µSmalltalk)
442 (nano‐ML)
interface, 586 (Molecule) object, 710 (µSmalltalk), S96 (µProlog)
introduction form, 384 (type systems) object language, 319 (µScheme in ML)
invariant, 586 (Molecule) object‐orientation, 587 (Molecule)
occurs check, S96 (µProlog)
judgment, 69 (Impcore) open recursion, 710 (µSmalltalk)
judgment form, 69 (Impcore) operational semantics, 70 (Impcore)
override, 710 (µSmalltalk)
kind, 384 (type systems)
parametric polymorphism, 384 (type
lambda abstraction, 173 (µScheme) systems)
lambda‐bound variable, 442 (nano‐ML) parsing, 70 (Impcore)
let‐bound variable, 442 (nano‐ML) pattern, 319 (µScheme in ML), 501 (µML)
list constructor, 318 (µScheme in ML) pattern matching, 319 (µScheme in ML),
live data, 289 (Garbage collection) 501 (µML)
location, 173 (µScheme) polymorphic type, 319 (µScheme in ML)
location semantics, 173 (µScheme) polymorphic type system, 384 (type sys‐
logic programming, S96 (µProlog) tems)
logical variable, S96 (µProlog) polymorphism, 173 (µScheme), 384 (type
loop invariant, 586 (Molecule) systems)
polytype, 385 (type systems), 442 (nano‐
magnitude, 709 (µSmalltalk) ML)
managed heap, 289 (Garbage collection) predefined function, 70 (Impcore)
map, 173 (µScheme) predicate, S96 (µProlog)
mark‐and‐sweep collector, 290 (Garbage predicate logic, S96 (µProlog)
collection) primitive function, 70 (Impcore)
mark bit, 290 (Garbage collection) principal type, 442 (nano‐ML)
memory management, explicit, 289 (Gar‐ private method, 710 (µSmalltalk)
bage collection) procedural programming, 70 (Impcore)
memory safety, 290 (Garbage collection) projection, 319 (µScheme in ML)
message, 709 (µSmalltalk) property, S96 (µProlog)
message not understood, 709 (µSmalltalk) proposition, S97 (µProlog)
message passing, 709 (µSmalltalk) propositional logic, S97 (µProlog)
protocol, 710 (µSmalltalk) substitution, 442 (nano‐ML), S97 (µProlog)
subtyping, 587 (Molecule)
quantified type, 385 (type systems) sum of products, 502 (µML)
query, S97 (µProlog) super, 711 (µSmalltalk)
superclass, 711 (µSmalltalk)
reachability, 290 (Garbage collection) syntactic category, 71 (Impcore)
receiver, 710 (µSmalltalk) syntactic form, 71 (Impcore)
reduction semantics, 245 (µScheme+) syntactic sugar, 71 (Impcore),
redundant pattern, 319 (µScheme in ML) 174 (µScheme) Key words
reference counting, 290 (Garbage collec‐
and phrases
tion) term, 385 (type systems), S97 (µProlog)
reflection, 711 (µSmalltalk) tricolor marking, 290 (Garbage collection) 747
relation, S97 (µProlog) type, 385 (type systems), 442 (nano‐ML)
rely‐guarantee reasoning, 587 (Molecule) type abbreviation, 320 (µScheme in ML),
representation invariant, 587 (Molecule), 502 (µML)
711 (µSmalltalk) type abstraction, 385 (type systems)
root, 290 (Garbage collection) type application, 385 (type systems)
rule, S97 (µProlog) type checker, 385 (type systems)
type constructor, 385 (type systems)
S‐expression, 174 (µScheme) type inference, 442 (nano‐ML)
scrutinee, 502 (µML) type scheme, 442 (nano‐ML)
self, 711 (µSmalltalk) type system, 385 (type systems)
shared mutable state, 174 (µScheme) type variable, 320 (µScheme in ML)
short‐circuit conditional, 320 (µScheme
in ML) undelimited continuation,
short‐circuit evaluation, 174 (µScheme) 245 (µScheme+)
single inheritance, 711 (µSmalltalk) unification, 442 (nano‐ML), S97 (µProlog)
small‐step semantics, 71 (Impcore), unreachable, 290 (Garbage collection)
245 (µScheme+) user‐defined type, 502 (µML)
soundness, S97 (µProlog)
stack allocation, 290 (Garbage collection) value constructor, 320 (µScheme in ML),
store, 174 (µScheme) 502 (µML)
structured operational semantics, value semantics, 174 (µScheme)
245 (µScheme+)
subclass, 711 (µSmalltalk) work per allocation, 290 (Garbage collec‐
subgoal, S97 (µProlog) tion)
Concept index
A (automatically generated function in abstraction(s), 527, 528, see also data ab‐
identifier cross‐reference), 39 straction
abbreviations, type, 529 immutable, 545
abstract classes, 622, 624, 656–662, 708 mutable, 545
Collection, 642 syntactic, see macros
Integer, 664 abstraction functions, 545, 549 (defined),
LargeInteger, 669 586, 708
Number, 663 circular lists, 673
abstract data types, see abstract types complex numbers, 551
abstract machine(s), 30, 67 data structures, typical and, 549–
further reading, 246 551
Impcore, 30 dictionaries, 549
for implementation, 246 examples, 552
µScheme, 144 natural numbers, 670, 671
µScheme+, 211 priority queues, 550, 552
µSmalltalk, 678–679 sets, 548
representations two‐dimensional points, 551
µSmalltalk, 686–687 abstraction, functional, 36
in semantics, 211 access paths, 561 (defined)
abstract‐machine semantics, 244 absolute, 559
abstract syntax, 14, 15, 67 environment lookup, 574
exceptions, 589 of modules or components, 559
Impcore, 27–28 pseudo‐bindings, 563, 574
µML, 485–486 relative, 575
µScheme (in ML), 306–307 accessor(s), see observers
µScheme (in C), 144–145 accessor functions for records, 107
µScheme+, 225–226 accumulating parameters, 99–101
µScheme+ stack frames, 225–226 ad hoc polymorphism, 132, 544, see also
µSmalltalk, 687–688 overloading
nano‐ML, 404–405 algebraic data types, 317, 501
Typed Impcore, 332–333 compile‐time checks, 499–500
Typed µScheme, 361 extensions, 503
abstract‐syntax trees, 27–28 generalized, 503
in C, 41–43 in µML, 466–476
abstract types, 455, 525, 538, 585 in Molecule, 535
in C, 39 origins, 502
as components of modules, 538 polymorphic, 462–463
design choices, 580–583 in real languages, 499–501
equality and, 634 recursive, 463–464
examples, 526 algebraic laws, 98
and exceptions, 583–584 association lists, 106, 112
in Java, 707 binary‐tree nodes, 109
limitations, 585 in calculational proofs, 115
objects vs, 625–627 case expressions, 476–477
proofs of correctness, 588 continuation‐passing style and,
in real languages, 580–583 138
749
algebraic laws (continued) append, 99 (µScheme), 475 (µML)
currying and uncurrying, 126 proof of laws, 114
equality of S‐expressions, 104 applicative languages, 402
finite maps, 112 apply primitive (exercise), 198
further reading, 175 arguments, variable number of, 169
has? (µScheme function), 103 arithmetic, multiprecision, see multi‐
higher‐order list functions, 130 precision arithmetic
if expressions, 112 arity
and imperative features, 403 µSmalltalk message names, 627
Concept index insertion sort, 101 primitives, 313
arrays
length of a list, 98
750 length of appended lists, 115
associative, see finite maps
implementation (µSmalltalk), 661–
list append, 99
662
list insertion, 101
semantics, 345
list primitives, 111
syntax, 343–345
list reversal, 100
type checking, 348
membership in S‐expressions, 103
types for, 343
notation for lists, condensed, 99 typing rules, 346–348
operator classifications and, 111– arrow types
112 typing rules, 348
pair types, 349 ascriptions
product types, 349 of a module type to a module, 564
proofs of, 114, 115 in Standard ML, 441
set functions, 105 type checking, 564
simplifying code with, 113 assignment
solving conjunctive normal form, to Boolean variables, 138
141, 142 satisfying a Boolean formula, 138
soundness, 65 assignment (syntactic form set)
in specifications, 547, 548 implementations
substitutions, 409–410 Impcore, 49
testable properties as, 110 µScheme, 156
tree‐node functions, 109 µScheme+, 230
tuple types, 349 operational semantics
uses, 175 Impcore, 34
algebraic specification, 547, 548 µScheme, 145
allocation, 259 µSmalltalk, 680
in functional languages, 205 small‐step semantics, 217
implementations, 162 (µScheme), typing rules
266 (µScheme+) Typed Impcore, 335
Typed µScheme, 363
interfaces, 154
association lists, 105–107, 646, S73
in µScheme+, 266
algebraic laws, 106, 112
mutability and, 545
attributes, 105
in procedural languages, 206
continuations and, 136–137
allof, 541, 552, 555
equality, 132–133
alpha‐conversion (renaming)
in full Scheme, 169
type variables, 368–369
keys, 105
and (∧, type intersection), 564 sets of, 132–135
and, meaning in ML, 303 associative arrays, see finite maps
answer(s), 708 ASTs, see abstract‐syntax trees
to messages, 610 (defined) @ (instantiation), 352, 358, 412
answer (verb form) examples, 358
in Smalltalk, 612 implicit, 413
answer types typing rules, 365
in continuation‐passing style, 138 atoms, 172, S95
APIs, 527, 586 in µScheme, 92
examples, 531–532 in Prolog, S43
autognostic principle, 626, 708 binary trees (continued)
automatic instantiation, 409 traversals (continued)
automatic memory management, 288 preorder, 110
benefits, 257 binding(s)
performance, 260 access paths, 563, 574
in Molecule type‐checking envi‐
B (basis function in identifier cross‐ ronments, 563
reference), 39 binding occurrences
backtracking type variables, 371 (defined)
using continuation‐passing style, black magic, decision procedure for, S45 Concept index
138–143 black objects, 261
visualizations, 139 in copying collection, 273 751
Backus‐Naur Form, 17 examples, 275
base types, 343 in mark‐sweep collection, 269
Typed Impcore, 333 blocks (µSmalltalk), 618, 627–629, 708
basis, 26 (defined), 67 bootstrapping, 698
Impcore, 29 closures and, 618
initial, see initial basis evaluation, 691
Typed µScheme, 382 operational semantics, 681
bearskins, 588 protocol, 630, 638–641
begin
BNF, 17
operational semantics Boolean(s)
Impcore, 35 Church encoding, 655
µScheme, 149 µSmalltalk, 630
µSmalltalk, 681 bootstrapping, 697–699
syntactic sugar, 167
boolean?, 92
typing rules
Boolean classes
nano‐ML, 413 (nondeterministic),
implementations, 655
446 (constraint‐based)
protocol, 639
Typed Impcore, 336
Boolean operators, short‐circuit, 164–
Typed µScheme, 363
165
behavior(s), µSmalltalk
Boolean satisfiability, 138–139
representation, 687
solver, 139–143
semantics, 678–679
bootstrapping, µSmalltalk
behavioral specifications, 527
blocks, Booleans, literals, 698–699
examples, 529, 547
bound occurrences
behavioral subtyping, 627, 708, 713
of type variables, 371
big‐step semantics, see operational se‐
bound type variables, 371 (defined)
mantics
bound variables, 315–317
bignums, see large integers
brackets
binary operations
square vs round, 117
on abstract types, 555–557
as tokens, 17
on objects, 662–673
primitives, embedded as, 313 breadth‐first traversals, 118–120
break, 202, 206–207 (examples)
binary search trees
exercises, 508–509 real implementations, 239
representation invariants, 550 Brown, Troy, 635
binary trees bugs, µScheme, false reports of, 154
algebraic laws, 109 Byrd boxes, 139, 175
encoded as S‐expressions, 109–110
exercises, 508–509 C programming
inference rules, to define, 109 advantages and disadvantages, 301
µScheme, 109–110 conventions, 39–40
traversals interfaces, 40
breadth‐first, 118–120 ML programming vs, 314–315
inorder, 181 scope rules, 74
level‐order, 118–120 static variables, 124
postorder, 181 caar, cadr, cdar, and so on, 96
calculational proofs, 114–116, see also Church‐Rosser theorem, 246
equational reasoning Circle (µSmalltalk class), 620
form, 114 circular lists, 673
organization, 115 abstraction function, 673
calculus representation invariants, 673
as a form of abstract machine, 241 class(es), 610, 708
call/cc, 172, 242–243, 244, 247, 255 (ex‐ abstract, 622
ercises) creation, 695
call stacks, 65, 239, 244, 288 definitions, 614
Concept index canvases (µSmalltalk), 618–619 modules vs, 588
capitalization representations, 686, 694–695
752 in C programs, 39–40 Class (µSmalltalk class), 696–697
of value constructors, 469–471 class Collection, 656–658
capture class definitions
of type variables, 369, 373 operational semantics, 685
in type‑lambda, 365 class hierarchies
capture‐avoiding substitution, 165–167, collections, 642
365, 373–375 integers, 651
implementations, 374–375 numbers, 649
specification (rules), 374 Smalltalk‐80, 704–707
for type variables, 365 class instance variables
type‑lambda and, 377–378
Smalltalk‐80, 704
car, 94, 172
class List, 674–677
implementations, 162
class Magnitude, 656
operational semantics, 151
class Metaclass, 696–697
origin of name, 94
class methods, 614, 708
cascade, message (Smalltalk‐80), 706
class Object, 696
case expressions, 457, 459, 501
class objects, 630
algebraic laws, 476–477
Class protocol, 638
benefits, 501
class protocols, 613
equational reasoning, 476–479
class True, 655
evaluation, 490–494
class UndefinedObject, 696
examples, 459–466, 475–476
class variables
operational semantics, 490–494
Smalltalk‐80, 704
semantics, informal, 468–469
classifications
type inference, 495–499
typing rules, 349, 495–496 (non‐ of operators, 111–112, 546
deterministic), 497–
clausal definitions, 318, 480 (defined),
499 (constraint‐based) 501, 516
catch (in µScheme+), 204
benefits, 501
catching an exception, 208 examples, 480–482
categories, syntactic, see syntactic cat‐ clauses (Prolog), S95
egories client code, 526 (defined), 527, 530, 586
cdar, caar, cadr, and so on, 96 closing
cdr, 94, 172 over free type variables, 414
operational semantics, 151 closures, 122–125, 172
origin of name, 94 application semantics, 147
CESK machine, 243, 246 further reading, 175
characteristic functions (of sets), 187 implementation, 122–124
check‑assert, 23, 26 in µSmalltalk, 691
check‑error, 23, 25 in operational semantics, 147, 681
check‑expect, 23–26 representations
in µSmalltalk using =, 635 C, 152
check‑principal‑type, 402 nano‐ML, 405
check‑type, 402 semantics, 147–149
checked run‐time errors, 40 static scoping and, 148
Church encodings CLU, 526
Booleans, 655 further reading, 588
CLU (continued) concrete syntax (continued)
mutable types vs immutable types, Typed µScheme, 352, 353
545 cond (Scheme form), 163–164
program correctness and, 585 conditional expressions
CNF , see conjunctive normal form algebraic laws, 112
code chunks, 39 implementations
coerce: method, 663 µScheme+, 230–231
coercions (µSmalltalk), 667–669 method dispatch vs, 623, 654–656
collection(s), 708 object‐oriented programming,
examples, 645 deprecated in, 655 Concept index
implementations, 656–662 operational semantics
inheritance, 649 Impcore, 34 753
µSmalltalk protocols, 642–649 µScheme, 149
Collection (µSmalltalk class), 656–658 small‐step semantics, 217–218
collection hierarchies, 642 type inference, 413, 418, 421
Smalltalk‐80, 705–706 typing rules
Collection protocol, 643, 644 nano‐ML, 413 (nondeterminis‐
coloring invariant, 261 (defined) tic), 418 (explicit substitutions),
Common Lisp, 89, 174, 713 421 (constraints)
CommonLoops, 713 Typed Impcore, 335
commutative diagrams, 477 Typed µScheme, 363
compilation, 68 conjunctions, 420
of pattern matching, 500–501, 503 constraint solving, 429
compiling with continuations, 247 conjunctive normal form, 139
complex operations, 555–557, 662–673, representation (µScheme), 140
709 cons, 94, 172
examples, 666 implementations, 161, 473
components of modules, 528 (defined) operational semantics, 151
declarations of, 538 origin of name, 94
exported, 538 cons cell, 94, 95 (defined)
of intersection types, 541 Cons protocol, 675
nested modules, 539 constraint(s), type‐equality, 441
public, 538 grammar, 420
strengthening, 570 implementations, 436
types, 538 trivial, 420
values, 538 in type inference, 420–428
composition typing judgment, 420
of functions, see function composi‐ unsolvable, 424
tion constraint satisfaction, 428 (defined)
of substitutions, 410–411 constraint solving, 418, 428–431, 441
conclusions conjunction, 429
in inference rules, 31 examples, 431
concrete syntax, 14, 15, 69 implementations, 436–437
curried functions, impact on, 126 simple type equality, 429–430
exceptions, 589 constructed values, 457 (defined), 501
Impcore, 17–19 µML, 457
Impcore vs C, 12 Molecule, 534
µML, 467 “constructor”
µScheme, 92, 93 pitfalls as a technical term, 111
µSmalltalk, 627–629 constructor(s), 111
rationale, 611 algebraic data types and, 457
Molecule, 535 of Molecule values, 533
core layer, 536 names for abstract syntax, 42
module layer, 537 smart (in binary tries), 513
nano‐ML, 404 constructor functions
partial application, impact on, 126 for records, 108
Smalltalk‐80, 702–704 contexts, evaluation, see evaluation con‐
Typed Impcore, 330–331 texts
continuation(s), 136–138, 244 copying garbage collection (continued)
blocks in Smalltalk as, 640 examples, 274–276
compiling with, 247 in µScheme+, 276–278
defined by evaluation context, 242 performance, 278–279
delimited, 243, S358–359 copying phase
entering, 242 in µScheme+ garbage collector,
exceptions and, 209–210 273
failure, 136, 139, S239 core languages, 26, 162, 214
first‐class, 242 layer in Molecule, 534
Concept index further reading, 246, 247 in µScheme, 120
in real languages, 242–243 Core µScheme+, 214
754 solutions, finding with, 142 operational semantics, 215–223
success, 136, 139 cost models of abstractions, 546
undelimited, 243, 245 crash, µScheme, causes of, 154
continuation‐passing style, 138–143 creators (class of operations), 111
for backtracking, 138–143 introduction forms and, 347
direct style vs, 138 in protocols, 614
further reading, 175 creators, producers, and observers, 111–
identifying, 138, 170 112
in Smalltalk, 610, 640 type systems and, 347
in Smalltalk conditionals, 655 curried functions, 125–127
continue, 202 concrete syntax and, 126
examples, 206–207 curried modules, 575
real implementations, 239 Curry‐Howard isomorphism, 347, 727–
contravariance 729
of function arrow in subtyping, currying and uncurrying, 125–127
606
algebraic laws, 126
control (control operator), 247
higher‐order functions, 126
control flow, 201
using continuations, 136–138
Darcy, Lord, investigator for the Anglo‐
control operators, 201–202, 206–210,
French empire, S45
244, 248–249 (exercises)
data abstraction, 455, 525, 586, 709
big‐step judgment forms, 677
in big‐step semantics, 679 benefits, 525
break, 202
binary operations and, 662–673
continue, 202
closed systems and, 626
control, 247 design, 545–554
long‑goto, 204 equality and, 634
long‑label, 204 examples, 525–526, 546–548
in µScheme+, 202–204 further reading, 587
µSmalltalk return, 677 in object‐oriented languages, 625–
operational semantics, 210, 221– 627
222 open systems and, 626
prompt, 247 data definitions
in real languages, 239–240 µML, 466, 471–472
reset, 247 typing rules, 487–489
return, 202 database (of Prolog clauses), S46
shift, 247 datatype definitions, see also data defini‐
throw, 202 tions
try‑catch, 204 desiderata, 457
conventions, coding generativity, 472
C, 39–40 µML, 471–473
ML, 301–303 typing, 471–473
CoordPair De Morgan’s laws
implementation, 615 for exists? and all?, 131
protocols, 613 in solving Boolean formulas, 189
copying garbage collection, 271–279 dead objects, 289
C code, 276–278 dead variables, 289
debugging domains
closures (µScheme), S331 of Molecule type‐checking envi‐
garbage collectors, 280–283 ronments, 563
by tracing calls and returns, 198 of substitutions, 410
by tracing evaluation, 224 dot notation, 528
by tracing message sends, 640, 641 double dispatch, 666–667
decidability of type inference, 401 duck typing, 627, 708
declarations dynamic dispatch, see method dispatch
of exported components, 538 dynamic scoping, 148
of manifest types, 530 dynamic typing, 327 Concept index
syntactic category of, 16
define eager evaluation, 241 755
desugared into lambda, 120 EBNF, 17, S9–10
explained, 22 effects, 110
in full Scheme, 169 either (elimination form for sum
This book was typeset by the author using XELATEX and the
Noweb system for literate programming. The main text
font is Source Serif Pro by Frank Grießhammer. The code
font is Input by David Jonathan Ross. The math font is
Computer Modern by Donald Ervin Knuth.