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.
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.
The types nat → nat and nat list may be thought of as being built from other types by the application of a type constructor, or type operator. These two examples differ from each other in that the function space type constructor takes two arguments, whereas the list type constructor takes only one. We may, for the sake of uniformity, think of types such as nat as being built by a type constructor of no arguments. More subtly, we may even think of the types ∀(t · τ) and ∃(t. τ) as being built up in the same way by regarding the quantifiers as higher-order type operators.
These seemingly disparate cases may be treated uniformly by enriching the syntactic structure of a language with a new layer of constructors. To ensure that constructors are used properly (for example, that the list constructor is given only one argument and that the function constructor is given two), we classify constructors by kinds. Constructors of a distinguished kind T are types, which may be used to classify expressions. To allow for multiargument and higher-order constructors, we also consider finite product and function kinds. (Later we consider even richer kinds.)
The distinction between constructors and kinds on one hand and types and expressions on the other reflects a fundamental separation between the static, and the dynamic phase of processing of a programming language, called the phase distinction. The static phase implements the statics, and the dynamic phase implements the dynamics.
A reference to an assignable a is a value, written as & a, of reference type that uniquely determines the assignable a. A reference to an assignable provides the capability to get or set the contents of that assignable, even if the assignable itself is not in scope at the point at which it is used. Two references may also be compared for equality to test whether they govern the same underlying assignable. If two references are equal, then setting one will affect the result of getting the other; if they are not equal, then setting one cannot influence the result of getting from the other. Two references that govern the same underlying assignable are said to be aliases. The possibility of aliasing complicates reasoning about the correctness of code that uses references, for we must always consider for any two references whether they might be aliases.
Reference types are compatible with both a scoped and a scope-free allocation of assignables. When assignables are scoped, the range of significance of a reference type must be limited to the scope of the assignable to which it refers. This may be achieved by declaring that reference types are immobile, so that they cannot be returned from the body of a declaration or stored in an assignable. Although ensuring adherence to the stack discipline, this restriction precludes the use of references to create mutable data structures, those whose structure can be altered during execution.
A future is a computation whose evaluation is initiated in advance of any demand for its value. Like a suspension, a future represents a value that is to be determined later. Unlike a suspension, a future is always evaluated, regardless of whether its value is actually required. In a sequential setting futures are of little interest; a future of type τ is just an expression of type τ. In a parallel setting, however, futures are of interest because they provide a means of initiating a parallel computation whose result is not needed until (presumably) much later, by which time it will have been completed.
The prototypical example of the use of futures is to implementing pipelining, a method for overlapping the stages of a multistage computation to the fullest extent possible. This minimizes the latency caused by one stage waiting for the completion of a previous stage by allowing the two stages to proceed in parallel until such time as an explicit dependency is encountered. Ideally, the computation of the result of an earlier stage is completed by the time a later stage requires it. At worst the later stage must be delayed until the earlier stage completes, incurring what is known as a pipeline stall.
A speculation is a delayed computation whose result may or may not be needed for the overall computation to finish. The dynamics for speculations executes suspended computations in parallel with the main thread of computation, without regard to whether the value of the speculation is actually required by the main thread.