To save content items to your account,
please confirm that you agree to abide by our usage policies.
If this is the first time you use this feature, you will be asked to authorise Cambridge Core to connect with your account.
Find out more about saving content to .
To save content items to your Kindle, first ensure no-reply@cambridge.org
is added to your Approved Personal Document E-mail List under your Personal Document Settings
on the Manage Your Content and Devices page of your Amazon account. Then enter the ‘name’ part
of your Kindle email address below.
Find out more about saving to your Kindle.
Note you can select to save to either the @free.kindle.com or @kindle.com variations.
‘@free.kindle.com’ emails are free but can only be saved to your device when it is connected to wi-fi.
‘@kindle.com’ emails can be delivered even when you are not connected to wi-fi, but note that service fees apply.
Every computation has the goal of returning a value to a certain entity that we call a continuation. This chapter explains that idea and its historic roots. We'll also define a new interpreter, one that makes continuations explicit. In doing so, we'll present various implementations in Lisp and Scheme and we'll go into greater depth about the programming style known as “Continuation Passing Style.” Lisp is distinctive among programming languages because of its elaborate forms for manipulating execution control. In some respects, that richness in Lisp will make this chapter seem like an enormous catalogue [Moz87] where you'll probably feel like you've seen a thousand and three control forms one by one. In other respects, however, we'll keep a veil over continuations, at least over how they are physically carried out. Our new interpreter will use objects to show the relatives of continuations and its control blocks in the evaluation stack.
The interpreters that we built in earlier chapters took an expression and an environment in order to determine the value of the expression. However, those interpreters were not capable of defining computations that included escapes, useful control structures that involve getting out of one context in order to get into another, more preferable one. In conventional programming, we use escapes principally to master the behavior of programs in case of unexpected errors, or to program by exceptions when we define a general behavior where the occurrence of a particular event interrupts the current calculation and sends it back to an appropriate place.
The exercises in this book are intended to deepen your understanding of ml and improve your programming skills. But such exercises cannot turn you into a programmer, let alone a software engineer. A project is more than a large programming exercise; it involves more than programming. It demands careful preparation: background study, analysis of requirements, design. The finished program should be evaluated fairly but thoroughly.
Each suggestion is little better than a hint, but with a little effort, can be developed into a proper proposal. Follow the attached references and prepare a project description including a statement of objectives, a provisional timetable and a list of required resources. The next stage is to write a detailed requirements analysis, listing all functions in sufficient detail to allow someone else to carry out eventual testing. Then specify the basic design; ml functors and signatures can describe the main components and their interfaces.
The preparatory phases outlined above might be done by the instructor, a student or a team of students. This depends upon the course aims, which might be concerned purely with ml, with project management, or with demonstrating some methodology of software engineering. The final evaluation might similarly be done by the instructor, the implementor or another team of students.
The evaluation should consider to what extent the program meets its objectives.
The most powerful techniques of functional programming are those that treat functions as data. Most functional languages give function values full rights, free of arbitrary restrictions. Like other values, functions may be arguments and results of other functions and may belong to pairs, lists and trees.
Procedural languages like Fortran and Pascal accept this idea as far as is convenient for the compiler writer. Functions may be arguments: say, the comparison to be used in sorting or a numerical function to be integrated. Even this restricted case is important.
A function is higher-order (or a functional) if it operates on other functions. For instance, the functional map applies a function to every element of a list, creating a new list. A sufficiently rich collection of functionals can express all functions without using variables. Functionals can be designed to construct parsers (see Chapter 9) and theorem proving strategies (see Chapter 10).
Infinite lists, whose elements are evaluated upon demand, can be implemented using functions as data. The tail of a lazy list is a function that, if called, produces another lazy list. A lazy list can be infinitely long and any finite number of its elements can be evaluated.
Chapter outline
The first half presents the essential programming techniques involving functions as data. The second half serves as an extended, practical example. Lazy lists can be represented in ml (despite its strict evaluation rule) by means of function values.
In the preceding chapter, there was a denotational interpreter that worked with extreme precision but remarkably slowly. This chapter analyzes the reasons for that slowness and offers a few new interpreters to correct that fault by pretreating programs. In short, we'll see a rudimentary compiler in this chapter. We'll successively analyze: the representation of lexical environments, the protocol for calling functions, and the reification of continuations. The pretreatment will identify and then eliminate computations that it judges static; it will produce a result that includes only those operations that it thinks necessary for execution. Specialized combinators are introduced for that purpose. They play the role of an intermediate language like a set of instructions for a hypothetical virtual machine.
The denotational interpreter of the preceding chapter culminated a series of interpreters leading to inexorably increasing precision. Now we'll have to correct that unbearable slowness. Still adhering to our technique of incremental modifications, particularly because the preceding denotational interpreter is the linguistic standard we have to conform to, we will present three successive interpreters, gradually relaxing some of the preliminary descriptive concerns for the benefit of the habits and customs of implementers.
A Fast Interpreter
To produce an efficient interpreter now, we'll assume that the implementation language contains a minimal number of concepts, notably, memory. We'll get rid of the one we added in Chapter 4 [see p. 111] since we added it just to explain the idea of memory.
After a brief review of λ-calculus, this chapter unveils denotational semantics in much of its glory. It introduces a new definition of Lisp—this time a denotational one—differing little from that of the preceding interpreter but this time associating each program with its meaning in the form of a respectable mathematical object: a term from λ-calculus.
What exactly is a program? A program is the description of a computing procedure that aims at a particular result or effect.
We often confuse a program with its executable incarnations on this or that machine; likewise, we sometimes treat the file containing the physical form of a program as its definition, though strictly speaking, we should keep these distinct.
A program is expressed in a language; the definition of a language gives a meaning to every program that can be expressed by means of that language. The meaning of a program is not merely the value that the program produces during execution since execution may entail reading or interacting with the exterior world in ways that we cannot know in advance. In fact, the meaning of a program is a much more fundamental property, its very essence.
The meaning of a program should be a mathematical object that can be manipulated. We'll judge as sound any transformation of a program, such as, for example, the transformation by boxes that we looked at earlier [see p. 115], if such a transformation is based on a demonstration that it preserves the meaning of every program to which it is applied.
Even though the literature about Lisp is abundant and already accessible to the reading public, nevertheless, this book still fills a need. The logical substratum where Lisp and Scheme are founded demand that modern users must read programs that use (and even abuse) advanced technology, that is, higher-order functions, objects, continuations, and so forth. Tomorrow's concepts will be built on these bases, so not knowing them blocks your path to the future.
To explain these entities, their origin, their variations, this book will go into great detail. Folklore tells us that even if a Lisp user knows the value of every construction in use, he or she generally does not know its cost. This work also intends to fill that mythical hole with an in-depth study of the semantics and implementation of various features of Lisp, made more solid by more than thirty years of history.
Lisp is an enjoyable language in which numerous fundamental and non-trivial problems can be studied simply. Along with ML, which is strongly typed and suffers few side effects, Lisp is the most representative of the applicative languages. The concepts that illustrate this class of languages absolutely must be mastered by students and computer scientists of today and tomorrow. Based on the idea of “function,” an idea that has matured over several centuries of mathematical research, applicative languages are omnipresent in computing; they appear in various forms, such as the composition of UN⋆X byte streams, the extension language for the EMACS editor, as well as other scripting languages.
Concrete data consists of constructions that can be inspected, taken apart, or joined to form larger constructions. Lists are an example of concrete data. We can test whether or not a list is empty, and divide a non-empty list into its head and tail. New elements can be joined to a list. This chapter introduces several other forms of concrete data, including trees and logical propositions.
The ml datatype declaration defines a new type along with its constructors. In an expression, constructors create values of a datatype; in patterns, constructions describe how to take such values apart. A datatype can represent a class consisting of distinct subclasses — like Pascal's variant records, but without their complications and insecurities. A recursive datatype typically represents a tree. Functions on datatypes are declared by pattern-matching.
The special datatype exn is the type of exceptions, which stand for error conditions. Errors can be signalled and trapped. An exception handler tests for particular errors by pattern-matching.
Chapter outline
This chapter describes datatypes, pattern-matching, exception handling and trees. It contains the following sections:
The datatype declaration. Datatypes, constructors and pattern-matching are illustrated through examples. To represent the King and his subjects, a single type person comprises four classes of individual and associates appropriate information with each.
Exceptions. These represent a class of error values. Exceptions can be declared for each possible error.
With each reprinting of this book, a dozen minor errors have silently disappeared. But a reprinting is no occasion for making improvements, however valuable, that would affect the page numbering: we should then have several slightly different, incompatible editions. An accumulation of major changes (and the Editor's urgings) have prompted this second edition.
As luck would have it, changes to ml have come about at the same time. ml has a new standard library and the language itself has been revised. It is worth stressing that the changes do not compromise ml's essential stability. Some obscure technical points have been simplified. Anomalies in the original definition have been corrected. Existing programs will run with few or no changes. The most visible changes are the new character type and a new set of top level library functions.
The new edition brings the book up to date and greatly improves the presentation. Modules are now introduced early — in Chapter 2 instead of Chapter 7 — and used throughout. This effects a change of emphasis, from data structures (say, binary search trees) to abstract types (say, dictionaries). A typical section introduces an abstract type and presents its ml signature. Then it explains the ideas underlying the implementation, and finally presents the code as an ml structure. Though reviewers have been kind to the first edition, many readers have requested such a restructuring.
Inored, abused, unjustly criticized, insufficiently justified (theoretically), macros are no less than one of the fundamental bases of Lisp and have contributed significantly to the longevity of the language itself. While functions abstract computations and objects abstract data, macros abstract the structure of programs. This chapter presents macros and explores the problems they pose. By far one of the least studied topics in Lisp, there is enormous variation in macros in the implementation of Lisp or Scheme. Though this chapter contains few programs, it tries to sweep through the domain where these little known beings—macros—have evolved.
Invented by Timothy P. Hart [SG93] in 1963 shortly after the publication of the Lisp 1.5 reference manual, macros turned out to be one of the essential ingredients of Lisp. Macros authorize programmers to imagine and implement the language appropriate to their own problem. Like mathematics, where we continually invent new abbreviations appropriate for expressing new concepts, dialects of Lisp extend the language by means of new syntactic constructions. Don't get me wrong: I'm not talking about augmenting the language by means of a library of functions covering a particular domain. A Lisp with a library of graphic functions for drawing is still Lisp and no more than Lisp. The kind of extensions I'm talking about introduce new syntactic forms that actually increase the programmer's power.
Extending a language means introducing new notation that announces that we can write X when we want to signify Y.
Most programmers know how hard it is to make a program work. In the 1970s, it became apparent that programmers could no longer cope with software projects that were growing ever more complex. Systems were delayed and cancelled; costs escalated. In response to this software crisis, several new methodologies have arisen — each an attempt to master the complexity of large systems.
Structured programming seeks to organize programs into simple parts with simple interfaces. An abstract data type lets the programmer view a data structure, with its operations, as a mathematical object. The next chapter, on modules, will say more about these topics.
Functional programming and logic programming aim to express computations directly in mathematics. The complicated machine state is made invisible; the programmer has to understand only one expression at a time.
Program correctness proofs are introduced in this chapter. Like the other responses to the software crisis, formal methods aim to increase our understanding. The first lesson is that a program only ‘works’ if it is correct with respect to its specification. Our minds cannot cope with the billions of steps in an execution. If the program is expressed in a mathematical form, however, then each stage of the computation can be described by a formula. Programs can be verified — proved correct — or derived from a specification. Most of the early work on program verification focused on Pascal and similar languages; functional programs are easier to reason about because they involve no machine state.
In a public lecture, C. A. R. Hoare (1989a) described his algorithm for finding the ith smallest integer in a collection. This algorithm is subtle, but Hoare described it with admirable clarity as a game of solitaire. Each playing card carried an integer. Moving cards from pile to pile by simple rules, the required integer could quickly be found.
Then Hoare changed the rules of the game. Each card occupied a fixed position, and could only be moved if exchanged with another card. This described the algorithm in terms of arrays. Arrays have great efficiency, but they also have a cost. They probably defeated much of the audience, as they defeat experienced programmers. Mills and Linger (1986) claim that programmers become more productive when arrays are restricted to stacks, queues, etc., without subscripting.
Functional programmers often process collections of items using lists. Like Hoare's stacks of cards, lists allow items to be dealt with one at a time, with great clarity. Lists are easy to understand mathematically, and turn out to be more efficient than commonly thought.
Chapter outline
This chapter describes how to program with lists in Standard ml. It presents several examples that would normally involve arrays, such as matrix operations and sorting.
The chapter contains the following sections:
Introduction to lists. The notion of list is introduced. Standard ml operates on lists using pattern-matching.
Commit protocols are used for concurrency control in distributed data bases. Thus they belong to the application layer. For an introduction to this area we recommend the book by Bernstein et al. [BHG87, Chapter 7].
If a data base is distributed over several sites, it is very possible that a data base operation which is logically a single action in fact involves more than one site of the data base. For example, consider the transfer of a sum of money sm from one bank account to another. The balance of the first bank account has to be decreased by sm, while the balance of the second has to be increased by sm. These two subactions might have to take place at different sites. It is imperative that both subactions are executed, and not one. If it is not possible to execute one of them, e.g. because its site is temporarily down, they should both be not executed.
In data base management such a logically single action is called a transaction, and it should behave as if it is an atomic action. At some point in the execution of the transaction it has to be decided whether the transaction is (going to be) executed as a whole and will never be revoked (commit), or that the transaction cannot be completed, and parts already done will be undone (abort). In general, an algorithm to ensure that the transaction can be viewed as an atomic action is called an atomic commitment protocol. Thus all processes participating in an atomic commitment protocol have to reach agreement upon whether to commit or to abort the transaction under consideration.