When we give a coercion semantics for a language with subtyping, there is a potential pitfall that we need to be careful to avoid. Suppose, for example, that we extend the present language with the base types Int, Bool, Float, and String. The following primitive coercions might all be useful:
0Bool <: Int0 = λb:Bool. if b then 1 else 0 0Int <: String0 = intToString
0Bool <: Float0 = λb:Bool. if b then 1.0 else 0.0 0Float <: String0 = floatToString
The functions intToString and floatToString are primitives that construct string representations of numbers. For the sake of the example, suppose that intToString(1) = "1", while floatToString(1.0) = "1.000".
Now, suppose we are asked to evaluate the term (λx:String.x) true;
using the coercion semantics. This term is typable, given the axioms above for the primitive types. In fact, it is typable in two distinct ways: we can either use subsumption to promote Bool to Int and then to String, to show that true is an appropriate argument to a function of type String→String, or we can promote Bool to Float and then to String. But if we translate these derivations into λ→, we get different behaviors. If we coerce true to type Int, we get 1, from which intToString yields the string "1". But if we instead coerce true to a float and then, using floatToString, to a String (following the structure of a typing derivation in which true : String is proved by going via Float), we obtain "1.000". But "1" and "1.000" are very different strings: they do not even have the same length. In other words, the choice of how to prove ? (λx:String. x) true : String affects the way the translated program behaves! But this choice is completely internal to the compiler-the programmer writes only terms, not derivations-so we have designed a language in which programmers cannot control or even predict the behavior of the programs they write.
The appropriate response to such problems is to impose an additional requirement, called coherence, on the definition of the translation functions.
15.6.4 Definition
A translation 0-0 from typing derivations in one language to terms in another is coherent if, for every pair of
derivations D1 and D2 with the same conclusion ? ? t : T, the translations 0D10 and 0D20 are behaviorally equivalent terms of the target language.
In particular, the translations given above (with no base types) are coherent. To recover coherence when we consider base types (with the axioms above), it suffices to change the definition of the floatToString primitive so that
floatToString(0.0) = "0" and floatToString(1.0) = "1".
Proving coherence, especially for more complex languages, can be a tricky business. See Reynolds (1980), Breazu-Tannen et al. (1991), Curien and Ghelli (1992), and Reynolds (1991).
[4]
Similar observations apply to accessing fields and methods of objects, in languages where object subtyping allows permutation. This is the reason that Java, for example, restricts subtyping between classes so that new fields can only be added at the end. Subtyping between interfaces (and between classes and interfaces) does allow permutation-if it did not, interfaces would be of hardly any use-and the manual explicitly warns that looking up a method from an interface will in general be slower than from a class.
15.7 Intersection and Union Types
A powerful refinement of the subtype relation can be obtained by adding an intersection operator to the language of types. Intersection types were invented by Coppo, Dezani, Sallé, and Pottinger (Coppo and Dezani-Ciancaglini, 1978; Coppo, Dezani-Ciancaglini, and Sallé, 1979; Pottinger, 1980). Accessible introductions can be found in Reynolds (1988, 1998b), Hindley (1992), and Pierce (1991b).
The inhabitants of the intersection type T1 ∧ T2 are terms belonging to both S and T-that is, T1 ∧ T2 is an order-theoretic meet (greatest lower bound) of T1 and T2. This intuition is captured by three new subtyping rules.
One additional rule allows a natural interaction between intersection and arrow types.
The intuition behind this rule is that, if we know a term has the function types S→T1 and S→T2, then we can certainly pass it an S and expect to get back both a T1 and a T2.
The power of intersection types is illustrated by the fact that, in a call-by-name variant of the simply typed lambda-calculus with subtyping and intersections, the set of untyped lambda-terms that can be assigned types is exactly the set of normalizing terms-i.e., a term is typable iff its evaluation terminates! This immediately implies that the type reconstruction problem (see Chapter 22) for calculi with intersections is undecidable.
More pragmatically, the interest of intersection types is that they support a form of finitary overloading. For example, we might assign the type (Nat→Nat→Nat) ∧ (Float→Float→Float) to an addition operator that can be used on both natural numbers and floats (using tag bits in the runtime representation of its arguments, for example, to select the correct instruction).
Unfortunately, the power of intersection types raises some difficult pragmatic issues for language designers. So far, only one full-scale language, Forsythe (Reynolds, 1988), has included intersections in their most general form. A restricted form known as refinement types may prove more manageable (Freeman and Pfenning, 1991; Pfenning, 1993b; Davies, 1997).
The dual notion of union types, T1 V T2, also turns out to be quite useful. Unlike sum and variant types (which, confusingly, are sometimes also called "unions"), T1 V T2 denotes the ordinary union of the set of values belonging to T1 and the set of values belonging to T2, with no added tag to identify the origin of a given element. Thus, Nat V Nat is actually just another name for Nat. Non-disjoint union types have long played an important role in program analysis (Palsberg and Pavlopoulou, 1998), but have featured in few programming languages (notably Algol 68; cf. van Wijngaarden et al., 1975); recently, though, they are increasingly being applied in the context of type systems for processing of "semistructured" database formats such as XML (Buneman and Pierce, 1998; Hosoya, Vouillon, and Pierce, 2001).
The main formal difference between disjoint and non-disjoint union types is that the latter lack any kind of case construct: if we know only that a value v has type T1 V T2, then the only operations we can safely perform on v are ones that make sense for both T1 and T2. (For example, if T1 and T2 are records, it makes sense to project v on their common fields.) The untagged union type in C is a source of type safety violations precisely because it ignores this restriction, allowing any operation on an element of T1 V T2 that makes sense for either T1 or T2.
15.8 Notes
The idea of subtyping in programming languages goes back to the 1960s, in Simula (Birtwistle, Dahl, Myhrhaug, and Nygaard, 1979) and its relatives. The first formal treatments are due to Reynolds (1980) and Cardelli (1984).
The typing and-especially-subtyping rules dealing with records are somewhat heavier than most of the other rules we have seen, involving either variable numbers of premises (one for each field) or additional mechanisms like
permutations on the indices of fields. There are many other ways of writing these rules, but all either suffer from similar complexity or else avoid it by introducing informal conventions (e.g., ellipsis: "l1:T1 ... ln:Tn". Frustration with this state of affairs led Cardelli and Mitchell to develop their calculus of Operations on Records (1991), in which the macro operation of creating a multi-field record is broken down into a basic empty record value plus an operation for adding a single field at a time. Additional operations such as in-place field update and record concatenation (Harper and Pierce, 1991) can also be considered in this setting. The typing rules for these operations be come rather subtle, especially in the presence of parametric polymorphism, so most language designers prefer to stick with ordinary records.
Nevertheless, Cardelli and Mitchell's system remains an important conceptual landmark. An alternative treatment of records based on row-variable polymorphism has been developed by (Wand 1987, 1988, 1989b), Rémy (1990, 1989, 1992), and others, and forms the basis for the object-oriented features of OCaml (Rémy and Vouillon, 1998; Vouillon, 2000).
The fundamental problem addressed by a type theory is to insure that programs have meaning. The fundamental problem caused by a type theory is that meaningful programs may not have meanings ascribed to them. The quest for richer type systems results from this tension. -Mark Mannasse
< Free Open Study >
Chapter 16: Metatheory of Subtyping
Overview
The definition in the previous chapter of the simply typed lambda-calculus with subtyping is not immediately suitable for implementation. Unlike the other calculi we have seen, the rules of this system are not syntax directed—they cannot just be "read from bottom to top" to yield a typechecking algorithm. The main culprits are the rules of subsumption (T-SUB) in the typing relation and transitivity (S-TRANS) in the subtype relation.
The reason T-SUB is problematic is that the term in its conclusion is specified as a bare metavariable t:
Every other typing rule specifies a term of some specific form—T-ABS applies only to lambda-abstractions, T-VAR only to variables, etc.—while T-SUB can be applied to any kind of term. This means that, if we are given a term t whose type we are trying to calculate, it will always be possible to apply either T-SUB or the other rule whose conclusion matches the shape of t.
S-TRANS is problematic for the same reason—its conclusion overlaps with the conclusions of all the other rules.
Since S and T are bare metavariables, we can potentially use S-TRANS as the final rule in a derivation of any subtyping statement. Thus, a naive "bottom to top" implementation of the subtyping rules would never know whether to try using this rule or whether to try another rule whose more specific conclusion also matches the two types whose membership in the subtype relation we are trying to check. [1]
There is one other problem with S-TRANS. Both of its premises mention the metavariable U, which does not appear in the conclusion. If we read the rule naively from bottom to top, it says that we should guess a type U and then attempt to show that S <: U and U <: T. Since there are an infinite number of Us that we could guess, this strategy has little hope of success.
The S-REFL rule also overlaps the conclusions of the other subtyping rules. This is less severe than the problems with T-SUB and S-TRANS: the reflexivity rule has no premises, so if it matches a subtyping statement we are trying to prove, we can succeed immediately. Still, it is another reason why the rules are not syntax directed.
The solution to all of these problems is to replace the ordinary (or declarative) subtyping and typing relations by two new relations, called the algorithmic subtyping and algorithmic typing relations, whose sets of inference rules are syntax directed. We then justify this switch by showing that the original subtyping and typing relations actually coincide with the algorithmic presentations: the statement S <: T is derivable from the algorithmic subtyping rules iff it is derivable from the declarative rules, and a term is typable by the algorithmic typing rules iff it is typable under the declarative rules.
We develop the algorithmic subtype relation in §16.1 and the algorithmic typing relation in §16.2. §16.3 addresses the special typechecking problems of multi-branch constructs like if...then...else, which require additional structure (the existence of least upper bounds, or joins, in the subtype relation). §16.4 considers the minimal type Bot.
[1]