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.
It frequently arises that the values of a type are partitioned into a variety of classes, each classifying data with a distinct internal structure. A good example is provided by the type of points in the plane, which may be classified according to whether they are represented in Cartesian or polar form. Both are represented by a pair of real numbers, but in the Cartesian case these are the x and y coordinates of the point, whereas in the polar case these are its distance r from the origin and its angle θ with the polar axis. A classified value is said to be an object, or instance, of its class. The class determines the type of the classified data, which are called the instance type of the class. The classified data itself is called the instance data of the object.
Functions that act on classified values are sometimes called methods. The behavior of a method is determined by the class of its argument. The method is said to dispatch on the class of the argument. Because it happens at run time, this is called dynamic dispatch. For example, the squared distance of a point from the origin is calculated differently according to whether the point is represented in Cartesian or polar form. In the former case the required distance is x2 + y2, whereas in the latter it is simply r itself.
Themotivation for introducing polymorphism was to enablemore programs to be written—those that are “generic” Chapter 20. Then if a program does not depend on the choice of types, we can code it by using polymorphism. Moreover, if we wish to insist that a program cannot depend on a choice of types, we demand that it be polymorphic. Thus polymorphism can be used both to expand the collection of programs we may write and also to limit the collection of programs that are permissible in a given context.
The restrictions imposed by polymorphic typing give rise to the experience that in a polymorphic functional language, if the types are correct, then the program is correct. Roughly speaking, if a function has a polymorphic type, then the strictures of type genericity vastly cut down the set of programs with that type. Thus if you have written a program with this type, it is quite likely to be the one you intended!
The technical foundation for these remarks is called parametricity. The goal of this chapter is to give an account of parametricity for ℒ{→ ∀} under a call-by-name interpretation.
Overview
We begin with an informal discussion of parametricity based on a “seat of the pants” understanding of the set of well-formed programs of a type.
Suppose that a function value f has the type ∀(t .t → t).
In constructive logic a proposition is true exactly when it has a proof, a derivation of it from axioms and assumptions, and is false exactly when it has a refutation, a derivation of a contradiction from the assumption that it is true. Constructive logic is a logic of positive evidence. To affirm or deny a proposition requires a proof, either of the proposition itself or of a contradiction, under the assumption that it has a proof. We are not always in a position to affirm or deny a proposition. An open problem is one for which we have neither a proof nor a refutation-so that, constructively speaking, it is neither true nor false.
In contrast, classical logic (the one we learned in school) is a logic of perfect information in which every proposition is either true or false. We may say that classical logic corresponds to “god's view” of the world-there are no open problems; rather, all propositions are either true or false. Put another way, to assert that every proposition is either true or false is to weaken the notion of truth to encompass all that is not false, dually to the constructively (and classically) valid interpretation of falsity as all that is not true. The symmetry between truth and falsity is appealing, but there is a price to pay for this: The meanings of the logical connectives are weaker in the classical case than in the constructive.
Most programming languages exhibit a phase distinction between the static and dynamic phases of processing. The static phase consists of parsing and type checking to ensure that the program is well-formed; the dynamic phase consists of execution of well-formed programs. A language is said to be safe exactly when well-formed programs are well-behaved when executed.
The static phase is specified by a statics comprising a collection of rules for deriving typing judgments stating that an expression is well-formed of a certain type. Types mediate the interaction between the constituent parts of a program by “predicting” some aspects of the execution behavior of the parts so that we may ensure they fit together properly at run time. Type safety tells us that these predictions are accurate; if not, the statics is considered to be improperly defined, and the language is deemed unsafe for execution.
This chapter presents the statics of the language ℒ{num str} as an illustration of the methodology that we employ throughout this book.
Syntax
When defining a language we are primarily concerned with its abstract syntax, specified by a collection of operators and their arities. The abstract syntax provides a systematic, unambiguous account of the hierarchical and binding structure of the language and is therefore to be considered the official presentation of the language. However, for the sake of clarity, it is also useful to specify minimal concrete syntax conventions without going through the trouble to set up a fully precise grammar for it.
As the name implies, a process is an ongoing computation that may interact with other processes by sending and receiving messages. From this point of view a concurrent computation has no definite “final outcome” but rather affords an opportunity for interaction that may well continue indefinitely. The notion of equivalence of processes must therefore be based on their potential for interaction, rather than on the “answer” that they may compute. Let P and Q be such that ⊢σP proc and ⊢σQ proc. We say that P and Q are equivalent, written as P ≈σQ, iff there is a bisimulation R such that P RσQ. A family of relations R = {Rσ}σ is a bisimulation iff whenever P may evolve to P′ taking the action α, then Q may also evolve to some process Q′ taking the same action such that P′ RσQ′, and, conversely, if Q may evolve to Q′ taking action α, then P may evolve to P′ taking the same action, and P′ RσQ′. This captures the idea that the two processes afford the same opportunities for interaction in that they each simulate each other's behavior with respect to their ability to interact with their environment.
Process Calculus
We consider a process calculus that consolidates the main ideas explored in Chapters 41 and 42. We assume as given an ambient language of expressions that includes the type clsfd of classified values (see Chapter 34).
In this chapter we build on Chapter 25 and consider the process of defining the dispatch matrix that determines the behavior of each method on each class. A common strategy is to build the dispatch matrix incrementally by adding new classes or methods to an existing dispatch matrix. To add a class requires that we define the behavior of each method on objects of that class, and to define a method requires that we define the behavior of that method on objects of each of the classes. The definitions of these behaviors may be given by any means available in the language. However, it is often suggested that a useful means of defining a new class is to inherit the behavior of another class on some methods and to override its behavior on others, resulting in an amalgam of the old and new behaviors. The new class is often called a subclass of the old class, which is then called the superclass. Similarly, a new method may be defined by inheriting the behavior of another method on some classes and overriding the behavior on others. By analogy, we may call the new method a submethod of a given supermethod. (It is also possible to admit multiple superclasses or multiple supermethods, but we confine our attention to single, rather than multiple, inheritance.) For simplicity we restrict attention to the simple, non-self-referential case in the following development.
Most contemporary programming languages are safe (or type safe, or strongly typed). Informally, this means that certain kinds of mismatches cannot arise during execution. For example, type safety for ℒ{num str} states that it will never arise that a number is to be added to a string, or that two numbers are to be concatenated, neither of which is meaningful.
In general type safety expresses the coherence between the statics and the dynamics. The statics may be seen as predicting that the value of an expression will have a certain form so that the dynamics of that expression is well-defined. Consequently, evaluation cannot “get stuck” in a state for which no transition is possible, corresponding in implementation terms to the absence of “illegal instruction” errors at execution time. This is proved by showing that each step of transition preserves typability and by showing that typable states are well-defined. Consequently, evaluation can never “go off into the weeds” and hence can never encounter an illegal instruction.
More precisely, type safety for ℒ{num str} may be stated as follows:
Theorem 6.1 (Type Safety).
If e : τ and e ↦ e′, then e′ : τ.
If e : τ, then either e val, or there exists e′ such that e ↦ e′.
The first part, called preservation, says that the steps of evaluation preserve typing; the second, called progress, ensures that well-typed expressions are either values or can be further evaluated.
So far we have mainly studied the statics and dynamics of programs in isolation, without regard to their interaction with the world. But to extend this analysis to even the most rudimentary forms of input and output requires that we consider external agents that interact with the program. After all, the whole purpose of a computer is, ultimately, to interact with a person!
To extend our investigations to interactive systems, we begin with the study of process calculi, which are abstract formalisms that capture the essence of interaction among independent agents. The development will proceed in stages, starting with simple action models, then extending to interacting concurrent processes, and finally to synchronous and asynchronous communication. The calculus consists of two main syntactic categories, processes and events. The basic form of process is one that awaits the arrival of an event. Processes are closed under parallel composition (the product of processes), replication, and declaration of a channel. The basic forms of event are signaling on a channel and querying a channel; these are later generalized to sending and receiving data on a channel. Events are closed under a finite choice (sum) of events. When enriched with types of messages and channel references, the process calculus may be seen to be universal in that it is at least as powerful as the untyped λ-caclulus.
Up to this point we have frequently encountered arbitrary choices in the dynamics of various language constructs. For example, when specifying the dynamics of pairs, we must choose, rather arbitrarily, between the lazy dynamics, in which all pairs are values regardless of the value status of their components, and the eager dynamics, in which a pair is a value only if its components are both values. We could even consider a half-eager (or, equivalently, half-lazy) dynamics, in which a pair is a value only if, say, the first component is a value, but without regard to the second.
Similar questions arise with sums (all injections are values, or only injections of values are values), recursive types (all folds are values, or only folds of values are values), and function types (functions should be called by-name or by-value). Whole languages are built around adherence to one policy or another. For example, Haskell decrees that products, sums, and recursive types are to be lazy and functions are to be called by name, whereas ML decrees the exact opposite policy. Not only are these choices arbitrary, but it is also unclear why they should be linked. For example, we could very sensibly decree that products, sums, and recursive types are lazy, yet impose a call-by-value discipline on functions. Or we could have eager products, sums, and recursive types, yet insist on call-by-name. It is not at all clear which of these points in the space of choices is right; each has its adherents, and each has its detractors.
A subtype relation is a preorder (reflexive and transitive relation) on types that validates the subsumption principle:
If τ′ is a subtype of τ, then a value of type τ′ may be provided whenever a value of type τ is required.
The subsumption principle relaxes the strictures of a type system to permit values of one type to be treated as values of another.
Experience shows that the subsumption principle, although useful as a general guide, can be tricky to apply correctly in practice. The key to getting it right is the principle of introduction and elimination. To determine whether a candidate subtyping relationship is sensible, it suffices to consider whether every introductory form of the subtype can be safely manipulated by every eliminatory form of the supertype. A subtyping principlemakes sense only if it passes this test; the proof of the type safety theorem for a given subtyping relation ensures that this is the case.
A good way to get a subtyping principle wrong is to think of a type merely as a set of values (generated by introductory forms) and to consider whether every value of the subtype can also be considered to be a value of the supertype. The intuition behind this approach is to think of subtyping as akin to the subset relation in ordinary mathematics. But, as we subsequently see, this can lead to serious errors, because it fails to take account of the eliminatory forms that are applicable to the supertype.