We use cookies to distinguish you from other users and to provide you with a better experience on our websites. Close this message to accept cookies or find out how to manage your cookie settings.
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.
The denotational semantics approach to the semantics of programming languages understands the language constructions by assigning elements of mathematical structures to them. The structures form so-called categories of domains and the study of their closure properties is the subject of domain theory [Sco70,Sco82,Plo83a,GS90,AJ94].
Typically, categories of domains consist of suitably complete partially ordered sets together with continuous maps. But, what is a category of domains? Our aim in this thesis is to answer this question by axiomatising the categorical structure needed on a category so that it can be considered a category of domains. Criteria required from categories of domains can be of the most varied sort. For example, we could ask them to
have fixed-point operators for endomorphisms and endofunctors;
have a rich collection of type constructors: coproducts, products, exponentials, powerdomains, dependent types, polymorphic types, etc;
have a Stone dual providing a logic of observable properties [Abr87, Vic89,Zha91];
have only computable maps [Sco76,Smy77,McC84,Ros86,Pho90a].
The criteria adopted here will be quite modest but rich enough for the denotational semantics of deterministic programming languages. For us a category of domains will be a category with the structure necessary to support the interpretation of the metalanguage FPC (a type theory with sums, products, exponentials and recursive types). And our axiomatic approach will aim not only at clarifying the categorical structure needed on a category for doing domain theory but also at relating such mathematical criteria with computational criteria.
This thesis is an investigation into axiomatic categorical domain theory as needed for the denotational semantics of deterministic programming languages.
To provide a direct semantic treatment of non-terminating computations, we make partiality the core of our theory. Thus, we focus on categories of partial maps. We study representability of partial maps and show its equivalence with classifiability. We observe that, once partiality is taken as primitive, a notion of approximation may be derived. In fact, two notions of approximation, contextual approximation and specialisation, based on testing and observing partial maps are considered and shown to coincide. Further we characterise when the approximation relation between partial maps is domain-theoretic in the (technical) sense that the category of partial maps Cpo-enriches with respect to it.
Concerning the semantics of type constructors in categories of partial maps, we present a characterisation of colimits of diagrams of total maps; study order-enriched partial cartesian closure; and provide conditions to guarantee the existence of the limits needed to solve recursive type equations. Concerning the semantics of recursive types, we motivate the study of enriched algebraic compactness and make it the central concept when interpreting recursive types. We establish the fundamental property of algebraically compact categories, namely that recursive types on them admit canonical interpretations, and show that in algebraically compact categories recursive types reduce to inductive types. Special attention is paid to Cpo-algebraic compactness, leading to the identification of a 2-category of kinds with very strong closure properties.
Everyone accepts that large programs should be organized as hierarchical modules. Standard ml's structures and signatures meet this requirement. Structures let us package up declarations of related types, values and functions. Signatures let us specify what components a structure must contain. Using structures and signatures in their simplest form we have treated examples ranging from the complex numbers in Chapter 2 to infinite sequences in Chapter 5.
A modular structure makes a program easier to understand. Better still, the modules ought to serve as interchangeable parts: replacing one module by an improved version should not require changing the rest of the program. Standard ml'sabstract types and functors can help us meet this objective too.
A module may reveal its internal details. When the module is replaced, other parts of the program that depend upon such details will fail. ml provides several ways of declaring an abstract type and related operations, while hiding the type's representation.
If structure B depends upon structure A, and we wish to replace A by another structure A′, we could edit the program text and recompile the program. That is satisfactory if A is obsolete and can be discarded. But what if A and A′ are both useful, such as structures for floating point arithmetic in different precisions?
ml lets us declare B to take a structure as a parameter.
In the previous chapters, with their spiraling build-up of repetition and variations, you may have felt like you were being subjected to the Lisp-equivalent of Ravel's Bolero. Even so, no doubt you noticed two motifs were missing: assignment and side effects. Some languages abhor both because of their nasty characteristics, but since Lisp dialects procure them, we really have to study them here. This chapter examines assignment in detail, along with other side effects that can be perpetrated. During these discussions, we'll necessarily digress to other topics, notably, equality and the semantics of quotations.
Coming from conventional algorithmic languages, assignment makes it more or less possible to modify the value associated with a variable. It induces a modification of the state of the program that must record, in one way or another, that such and such a variable has a value other than its preceding one. For those who have a taste for imperative languages, the meaning we could attribute to assignment seems simple enough. Nevertheless, this chapter will show that the presence of closures as well as the heritage of λ-calculus complicates the ideas of binding and variables.
The major problem in defining assignment (and side effects, too) is choosing a formalism independent of the traits that we want to define. As a consequence, neither assignment nor side effects can appear in the definition.
Once again, here's a chapter about compilation, but this time, we'll look at new techniques, notably, flat environments, and we have a new target language: C. This chapter takes up a few of the problems of this odd couple. This strange marriage has certain advantages, like free optimizations of the compilation at a very low level or freely and widely available libraries of immense size. However, there are some thorns among the roses, such as the fact that we can no longer guarantee tail recursion, and we have a hard time with garbage collection.
Compiling into a high-level language like C is interesting in more ways than one. Since the target language is so rich, we can hope for a translation that is closer to the original than would be some shapeless, linear salmagundi. Since C is available on practically any machine, the code we produce has a good chance of being portable. Moreover, any optimizations that such a compiler can achieve are automatically and implicitly available to us. This fact is particularly important in the case of C, where there are compilers that carry out a great many optimizations with respect to allocating registers, laying out code, or choosing modes of address—all things that we could ignore when we focused on only one source language.
On the other hand, choosing a high-level language as the target imposes certain philosophic and pragmatic constraints as well.
This book originated in lectures on Standard ml and functional programming. It can still be regarded as a text on functional programming — one with a pragmatic orientation, in contrast to the rather idealistic books that are the norm — but it is primarily a guide to the effective use of ml. It even discusses ml's imperative features.
Some of the material requires an understanding of discrete mathematics: elementary logic and set theory. Readers will find it easier if they already have some programming experience, but this is not essential.
The book is a programming manual, not a reference manual; it covers the major aspects of ml without getting bogged down with every detail. It devotes some time to theoretical principles, but is mainly concerned with efficient algorithms and practical programming.
The organization reflects my experience with teaching. Higher-order functions appear late, in Chapter 5. They are usually introduced at the very beginning with some contrived example that only confuses students. Higher-order functions are conceptually difficult and require thorough preparation. This book begins with basic types, lists and trees. When higher-order functions are reached, a host of motivating examples is at hand.
The exercises vary greatly in difficulty. They are not intended for assessing students, but for providing practice, broadening the material and provoking discussion.
Overview of the book. Most chapters are devoted to aspects of ml. Chapter 1 introduces the ideas behind functional programming and surveys the history of ml.
Functional programming has its merits, but imperative programming is here to stay. It is the most natural way to perform input and output. Some programs are specifically concerned with managing state: a chess program must keep track of where the pieces are! Some classical data structures, such as hash tables, work by updating arrays and pointers.
Standard ml's imperative features include references, arrays and commands for input and output. They support imperative programming in full generality, though with a flavour unique to ml. Looping is expressed by recursion or using a while construct. References behave differently from Pascal and C pointers; above all, they are secure.
Imperative features are compatible with functional programming. References and arrays can serve in functions and data structures that exhibit purely functional behaviour. We shall code sequences (lazy lists) using references to store each element. This avoids wasteful recomputation, which is a defect of the sequences of Section 5.12. We shall code functional arrays (where updating creates a new array) with the help of mutable arrays. This representation of functional arrays can be far more efficient than the binary tree approach of Section 4.15.
A typical ml program is largely functional. It retains many of the advantages of functional programming, including readability and even efficiency: garbage collection can be faster for immutable objects. Even for imperative programming, ml has advantages over conventional languages.
The first ml compiler was built in 1974. As the user community grew, various dialects began to appear. The ml community then got together to develop and promote a common language, Standard ml — sometimes called sml, or just ml. Good Standard ml compilers are available.
Standard ml has become remarkably popular in a short time. Universities around the world have adopted it as the first programming language to teach to students. Developers of substantial applications have chosen it as their implementation language. One could explain this popularity by saying that ml makes it easy to write clear, reliable programs. For a more satisfying explanation, let us examine how we look at computer systems.
Computers are enormously complex. The hardware and software found in a typical workstation are more than one mind can fully comprehend. Different people understand the workstation on different levels. To the user, the workstation is a word processor or spreadsheet. To the repair crew, it is a box containing a power supply, circuit boards, etc. To the machine language programmer, the workstation provides a large store of bytes, connected to a processor that can perform arithmetic and logical operations. The applications programmer understands the workstation through the medium of the chosen programming language.
Here we take ‘spreadsheet’, ‘power supply’ and ‘processor’ as ideal, abstract concepts. We think of them in terms of their functions and limitations, but not in terms of how they are built.
Uniquely characteristic of Lisp is its evaluation mechanism: eval. Although this book talks relentlessly about evaluation, we haven't said a word yet about the problem of making evaluation available to programmers. Evaluation poses a number of problems with respect to specification, integrity, and linguistics. Some people are thinking of all these problems when they say concisely, “eval is evil” Catching its genius in a useful form is the first step toward programming reflection, a topic this chapter also covers.
For 271 pages now, we've been presenting various interpreters detailing the core of the evaluation mechanism. For most of them, making the evaluation mechanism accessible to programmers is trivial, a task requiring very little code. That's what implementers have been doing for ages. The existence of such a mechanism [see p. 2] was surely one of the goals in creating Lisp. From the very beginning of the sixties, in fact, making the eval function explicit showed up in the writings of the founders, such as [McC60, MAE+62].
Explicit evaluation is fundamental, supporting as it does so many effects, notably, a powerful system of macros, immersion of the programming environment within the language, and pronounced reflection. Of course, explicit evaluation also has some defects such as macros, a programming environment right inside the language, and invasive reflection. Like a magic djinn, explicit evaluation can be both useful and dangerous.
Objects! Oh, where would we be without them? This chapter defines the object system that we've used throughout this book. We deliberately restricted ourselves to a limited set of characteristics so that we would not overburden the description which will follow here. In fact, as Rabelais would say, we want to limit it to its sustantificque mouelle, that is, to its very essence.
This object system is called Meroon. Such a system is complicated and demands considerable attention if we want it to be simultaneously efficient and portable. As a result, the system is endowed with a structure strongly influenced, maybe even distorted, by our worries about portability. To compensate for that, we're actually going to show you a reduced version of Meroon, and we'll call that reduced version Meroonet.
Lisp and objects have a long history in common. It begins with one of the first object languages, Smalltalk 72, which was first implemented in Lisp. Since that time, Lisp, as an excellent development language, has served as the cradle for innumerable studies about objects. We'll mention only a couple: Flavors, developed by Symbolics for a windowing system on Lisp machines, experimented with multiple inheritance; Loops, created at Xerox Pare, introduced the idea of generic functions. Those efforts culminated in the definition of CLOS (Common Lisp Object System) and of TEΛOΣ (the EuLisp object system). These latter two systems bring together most of the characteristics of the preceding object systems while immersing them in the typing system of the underlying language.
This chapter brings together all the concepts we have learned so far. For an extended example, it presents a collection of modules to implement the λ-calculus as a primitive functional programming language. Terms of the λ-calculus can be parsed, evaluated and the result displayed. It is hardly a practical language. Trivial arithmetic calculations employ unary notation and take minutes. However, its implementation involves many fundamental techniques: parsing, representing bound variables and reducing expressions to normal form. These techniques can be applied to theorem proving and computer algebra.
Chapter outline
We consider parsing and two interpreters for λ-terms, with an overview of the λ-calculus. The chapter contains the following sections:
A functional parser. An ml functor implements top-down recursive descent parsing. Parsers can be combined using infix operators that resemble the symbols for combining grammatical phrases.
Introducing the λ-calculus. Terms of this calculus can express functional programs. They can be evaluated using either the call-by-value or the call-by-name mechanism. Substitution must be performed carefully, avoiding variable name clashes.
Representing λ-terms inml. Substitution, parsing and pretty printing are implemented as ml structures.
The λ-calculus as a programming language. Typical data structures of functional languages, including infinite lists, are encoded in the λ-calculus. The evaluation of recursive functions is demonstrated.
A functional parser
Before discussing the λ-calculus, let us consider how to write scanners and parsers in a functional style.
Since functions occupy a central place in Lisp, and because their efficiency is so crucial, there have been many experiments and a great deal of research about functions. Indeed, some of those experiments continue today. This chapter explains various ways of thinking about functions and functional applications. It will carry us up to what we'll call Lisp1 or Lisp2, their differences depending on the concept of separate name spaces. The chapter closes with a look at recursion and its implementation in these various contexts.
Among all the objects that an evaluator can handle, a function represents a very special case. This basic type has a special creator, lambda, and at least one legal operation: application. We could hardly constrain a type less without stripping away all its utility. Incidentally, this fact—that it has few qualities—makes a function particularly attractive for specifications or encapsulations because it is opaque and thus allows only what it is programmed for. We can, for example, use functions to represent objects that have fields and methods (that is, data members and member functions) as in [AR88]. Scheme-users are particularly appreciative of functions.
Attempts to increase the efficiency of functions have motivated many, often incompatible, variations. Historically, Lisp 1.5 [MAE+62] did not recognize the idea of a functional object. Its internals were such, by the way, that a variable, a function, a macro—all three—could co-exist with the same name, and at the same time, the three were represented with different properties (APVAL, EXPR, or MACRO) on the P-list of the associated symbol.