On this page:
3.1 Syntax
3.2 Dynamic semantics
3.3 Static semantics
3.3.1 Subtyping
3.3.2 Type safety
3.4 Compiling with coercions

3 λ-sub: subtyping with records

3.1 Syntax

Extending STLC with records is straightforward. First, we extend the syntax of types and terms, using for record field labels:

A record type lists field names with their types; assume the field names are not repeated within a record. A record expression lists field names with expressions whose values will fill the fields. A projection expression projects the value of the named field from a record.

3.2 Dynamic semantics

The dynamics are straightforward. We extend values to include records where every field contains a value. We extend evaluation contexts to evaluate the fields of a record from left to right.

.

Then we add one reduction rule, for projecting the field from a record:

3.3 Static semantics

The simplest way to type records is to add one rule for each new expression form and keep the rest of the language the same:

This works, but it’s not as expressive as we might like. Consider a function . It takes a record of one field and projects out that field. But is there any reason we shouldn’t be able to use this function on a record with more fields than ? Subtyping captures that intuition, allowing us to formalize it and prove it sound.

3.3.1 Subtyping

To do this, we define the subtype relation , which related pairs of types. Intuititively means that a may be used wherever a is required.

First, is a subtype of itself:

Second, function types are contravariant in the domain and covariant in the arguments:

Exercise 23. Suppose that . Consider the types , , , and . Which of these are subtypes of which others? Does this make sense?

Finally, records provide subtyping by allowing the forgetting of fields (this is called width subtyping) and by subtyping within individual fields (depth subtyping). We can express this with three rules:

Rule [rec-empty] says that the empty record is a subtype of itself; we need this as a base case. Rule [rec-width] says that supertype records may have fields that are missing from their subtypes. Rule [rec-depth] says that when records have a common member then the types of the fields must be subtypes.

Exercise 24. Prove that is a preorder, that is, reflexive and transitive.

The idea of subtyping is that we can apply it everywhere. If we can conclude that and then we should be able to conclude that . It’s possible to add such a rule, and it works fine theoretically, but because the rule is not syntax directed, it can be difficult to implement. In fact, the only place in our current language that we need subtyping is in the application rule, so we replace the STLC application rule with this:

3.3.2 Type safety

Subtyping changes our preservation theorem somewhat, because reduction can cause type refinement. (That is, we learn more type information.) Here is the updated preservation theorem:

Theorem (Preservation). If and then there exists some such that and .

Before we can prove it, we update the replacement and substitution lemmas as follows:

Lemma (Replacement). If , then for some type . Furthermore, for any such that for , for some such that .

Proof. By induction on . The interesting cases are for application:

Lemma (Substitution). If and where then for .

Proof. By induction on the derivation of the typing of :

Proof (of preservation). By cases on the reduction relation. There are two cases:

QED.

Lemma (Canonical forms).

If , then:

Proof. By induction on the structure of the typing derivation. Only four rules form values, and those rules correspond to the conditions of the lemma.

Lemma (Progress). If then either is a value or for some term .

Proof. By induction on the typing derivation:

Theorem (Type safety). λ-sub is type safe.

Proof. By progess and preservation.

3.4 Compiling with coercions

To say that is to say that a can be used wherever a is expected, but do our run-time representations actually make that true? In some languages yes, but in many languages no. We might not want, for example, for record operations to have to do a (linear) search of field names at run time, but instead to fix the offset at compile time. Such a representation choice is not incompable with subtyping, if we are willing to interpret subtyping as a coercion between potentially different underlying representation types. For example, record type is a subtype of record type . The former is represented by a 3-element vector containing the values of fields a, b, and c, whereas the rather is represented as a 2-element vector containing the values of fields a and c. We cannot use an instance of the former as the latter directly, but we can coerce it. The coercion between two types in the subtype relationship is witnessed by the function converting the subtype to the supertype.

In particular, the witness to the fact that is the identity function on type :

To witness an arrow subtyping, we build a function that applies the witness to the domain coercion to the argument and the witness to the codomain coercion to the result of the coerced function:

The empty record is a supertype of itself, by the identity coercion:

In subtyping records, we can skip fields and not include them in the supertype:

The depth-subyping record case is hairy. We convert record types by converting one element and then recursively converting the rest of the record, and then reassembling the desired result:

The typing rules now translate from a language with subtyping to a language that doesn’t use subtyping. All of the rules except [app] just translate each term by homomorphically translating the subterms:

The only interesting rule is [app], which includes subtyping. It generates the coercion for the particular subtyping used, and then applies that to coerce the argument to the function:

Exercise 25. Define a Point as a record with fields x and y, which are integers. Define a ColorPoint as a Point with an additional field, the color, which is a string. Define a function that takes a Point. Show that your function can be used on a ColorPoint.