0% found this document useful (0 votes)
87 views7 pages

Lecture Notes On Subtyping: 15-312: Foundations of Programming Languages Frank Pfenning October 19, 2004

Subtyping is a fundamental concept in programming language design that allows more concise and readable programs by eliminating explicit type conversions. There are two main interpretations of subtyping: subset interpretation, where a subtype contains all values of the supertype, and coercion interpretation, where a function converts values of a subtype to the supertype. The lecture discusses the principles of subtyping such as reflexivity, transitivity, and coherence under the coercion interpretation. It also examines how subtyping interacts with types like pairs and functions.

Uploaded by

jiawen liu
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
87 views7 pages

Lecture Notes On Subtyping: 15-312: Foundations of Programming Languages Frank Pfenning October 19, 2004

Subtyping is a fundamental concept in programming language design that allows more concise and readable programs by eliminating explicit type conversions. There are two main interpretations of subtyping: subset interpretation, where a subtype contains all values of the supertype, and coercion interpretation, where a function converts values of a subtype to the supertype. The lecture discusses the principles of subtyping such as reflexivity, transitivity, and coherence under the coercion interpretation. It also examines how subtyping interacts with types like pairs and functions.

Uploaded by

jiawen liu
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Lecture Notes on

Subtyping

15-312: Foundations of Programming Languages


Frank Pfenning

Lecture 14
October 19, 2004

Subtyping is a fundamental idea in the design of programming lan-


guages. It can allow us to write more concise and readable programs by
eliminating the need to convert explicitly between elements of types. It can
also be used to express more properties of programs. Finally, it is absolutely
fundamental in object-oriented programming where the notion of subclass
is closely tied to the notion of subtype.
Which subtyping relationships we want to integrate into a language
depends on many factors. Besides some theoretical properties we want
to satisfy, we also have to consider the pragmatics of type-checking and
the operational semantics. In this lecture we are interested in isolating the
fundamental principles that must underly various forms of subtyping. We
will then see different instances of how these principles can be applied in
practice.
We write τ ≤ σ to express that τ is a subtype of σ. The fundamental
principle of subtyping says:

If τ ≤ σ then wherever a value of type σ is required, we can use a


value of type τ instead.

This can be refined into two more specific statements, depending on the
form of subtyping used.

Subset Interpretation. If τ ≤ σ then every value of type τ is also a


value of type σ.

L ECTURE N OTES O CTOBER 19, 2004


L14.2 Subtyping

As an example, consider both empty and non-empty lists as subtypes


of the type of lists. This is because an empty list is clearly a list, and a non-
empty lists is also a list. One can see that with a subset interpretation of
subtyping one can track properties of values.

Coercion Interpretation. If τ ≤ σ then every value of type τ can be


converted (coerced) to a value of type σ in a unique way.

As an example, consider integers as a subtype of floating point num-


bers. This interpretation is possible because there is a unique way we can
convert an integer to a corresponding floating point representation (ignor-
ing questions of size bounds). Therefore, coercive subtyping allows us to
omit explicit calls to functions that perform the coercion. However, we
have to be careful to guarantee the coerced value is unique, because oth-
erwise the result of a computation may be ambiguous. For example, if
we want to say that both integers and floating point numbers are also a
subtype of strings, and the coercion yields the printed representation, we
violate the uniqueness guarantee. This is because we can coerce 3 to "3"
since int ≤ string or 3 to 3.0 and then to "3.0" using first int ≤ float and
then float ≤ string. We call a language that satisfies the uniqueness property
coherent; incoherent languages are poor from the design point of view and
can lead to many practical problems. We therefore require the coherence
from the start.
Note that both forms of subtyping satisfy the fundamental principle,
but that the coercion interpretation is more difficult to achieve than subset
interpretation, because we have to verify uniqueness of coercions. Because
it is somewhat richer, we concentrate in this lecture on working out a con-
crete system of subtyping under the coercion interpretation.
First, some general laws that are independent of whether we choose a
subset or coercion interpretation. The defining property of subtyping can
be expressed in the calculus by the rule of subsumption.
Γ`e:τ τ ≤σ
subsume
Γ`e:σ

Secondly, we have reflexivity and transitivity of subtyping.


τ1 ≤ τ2 τ2 ≤ τ3
ref l trans
τ ≤τ τ1 ≤ τ3

Let us carefully justify these principles. Under the subset interpretation


τ ≤ τ follows from A ⊆ A for any set of values A. Transitivity follows from

L ECTURE N OTES O CTOBER 19, 2004


Subtyping L14.3

the transitivity of the subset relation. Under the coercion interpretation,


the identity function coerces from τ to τ for any τ . And we can validate
transitivity by composition of functions.
To make the latter considerations concrete, we annotate the subtyping
judgment with a coercion and we calculate this coercion in each case. We
write f : τ ≤ σ is f is a coercion from τ to σ. Note that coercions f are
always closed, that is, contain no free variables, so no context is necessary.

f : τ1 ≤ τ2 g : τ2 ≤ τ3
ref l trans
λx.x : τ ≤ τ λx.g(f (x)) : τ1 ≤ τ3

The three laws we have are essentially all the general laws that can be
formulated in this manner. Coherence is stated in a way that is similar to a
substitution principle.

Coherence. If f : τ ≤ σ and g : τ ≤ σ then f ' g : τ → σ.

Here, extensional equality f ' g : τ is defined inductively on type τ .


Note that all coercions should terminate and not have any effects. We show
the cases for functions, pairs, and primitive types.

1. e ' e0 : int if e 7→∗ num(n) and e0 7→∗ num(n0 ) and n = n0 .

2. e ' e0 : τ1 × τ2 if fst(e) ' fst(e0 ) : τ1 and snd(e) ' snd(e0 ) : τ2 .

3. e ' e0 : τ1 → τ2 if for any e1 ' e01 : τ1 we have e e1 ' e0 e01 : τ2 .

As a particular example of subtyping, consider int ≤ float. We call the


particular coercion itof : int → float.

int ≤ float itof : int ≤ float

In order to use these functions, consider two versions of the addition


operation: one for integers and one for floating point numbers. We avoid
overloading here, which is subject of another lecture.

Γ ` e1 : int Γ ` e2 : int Γ ` e1 : float Γ ` e2 : float


Γ ` e1 + e2 : int Γ ` e1 +. e2 : float

Now an expression such as 2 + 3.0 is ill-typed, since the second argu-


ment is a floating point number and floating point numbers in general can-
not be coerced to integers. However, the expression 2 +. 3.0 is well-typed

L ECTURE N OTES O CTOBER 19, 2004


L14.4 Subtyping

because the first argument 2 can be coerced to the floating point number
2.0 by applying itof. Concretely:

` 2 : int int ≤ float


` 2 : float ` 3.0 : float
` 2 +. 3.0 : float

So far we have avoided a discussion of the operational semantics, but


we can see that (a) under the subset interpretation the operational seman-
tics remains the same as without subtyping, and (b) under the coercion in-
terpretation the operational semantics must apply the coercion functions.
That is, we cannot define the operational semantics directly on expressions,
because only the subtyping derivation will contain the necessary infor-
mation on how and where to apply the coercions. We do not formalize
the translation from subtyping derivations with coercions to the language
without, but we show it by example. In the case above we have

` 2 : int itof : int ≤ float


` itof(2) : float ` 3.0 : float
` itof(2) +. 3.0 : float

The subsumption rule with annotations then looks like


Γ`e:τ f :τ ≤σ
Γ ` f (e) : σ

so we interpret f : τ ≤ σ as f : τ → σ. However, typing derivations


are not unique. As written, however, it is not quite right because the source
code does not contain f , only the result of type checking. This process is
generally called elaboration and will occupy us further in the next section.
Writing out coercions as we did above, we could have

λx.x : int ≤ int itof : int ≤ float


` 2 : int λy.itof((λx.x)(y)) : int ≤ float
` (λy.itof((λx.x)(y)))(2) : float ` 3.0 : float
` (λy.itof((λx.x)(y)))(2) +. 3.0 : float

This alternative compilation will behave identically to the first one, itof
and λy.itof((λx.x)(y)) are observationally equivalent. To see this, apply
both sides to a value v. Then the one side yiels itof(v), the other side

(λy.itof((λx.x)(y)))v 7→ itof((λx.x)v) 7→ itof(v)

L ECTURE N OTES O CTOBER 19, 2004


Subtyping L14.5

The fact that the particular chosen typing derivation does not affect the
behavior of the compiled expressions (where coercions are explicit) is the
subject of the coherence theorem for a language. This is a more precise ex-
pression of the “uniqueness” required in the defining property for coercive
subtyping.
At this point we have general laws for typing (subsumption) and sub-
typing (reflexivity and transitivity). But how does subtyping interact with
pairs, functions, and other constructs? We start with pairs. We can coerce a
value of type τ1 ×τ2 to a value of type σ1 ×σ2 if we can coerce the individual
components appropriately. That is:

τ1 ≤ σ 1 τ2 ≤ σ 2
τ1 × τ2 ≤ σ 1 × σ 2

With explicit coercions:

f1 : τ1 ≤ σ1 f2 : τ2 ≤ σ2
λp.pair(f1 (fst(p)), f2 (snd(p))) : τ1 × τ2 ≤ σ1 × σ2

Functions are somewhat trickier. We know that int ≤ float. It should


therefore be clear that int → int ≤ int → float, because we can coerce the
output of the function of the left to the required output for the function on
the right. So
λh.λx.itof(h(x)) : int → int ≤ int → float
Perhaps surprisingly, we have

float → int ≤ int → int

because we can obtain a function of the type on the right from a function
on the left by coercing the argument:

λh.λx.h(itof(x)) : float → int ≤ int → int

Putting these two ideas together we get

λh.λx.itof(h(itof(x))) : float → int ≤ int → float

In the general case, we obtain the following rule:

σ 1 ≤ τ1 τ2 ≤ σ 2
τ1 → τ2 ≤ σ 1 → σ 2

L ECTURE N OTES O CTOBER 19, 2004


L14.6 Subtyping

With coercion functions:


f1 : σ1 ≤ τ1 f2 : τ2 ≤ σ2
λh.λx.f2 (h(f1 (x))) : τ1 → τ2 ≤ σ1 → σ2

The fact that the subtyping relationship flips in the left premise is called
contravariance. We say that function subtyping is contravariant in the argu-
ment and covariant in the result. Subtyping of pairs, on the other hand, is
covariant in both components.
Mutable reference can be neither covariant nor contravariant. As simple
counterexamples, consider the following pieces of code.
The first one assumes that τ ref ≤ σ ref if σ ≤ τ , that is reference sub-
typing is contravariant.

let val r = ref 2.1 (* r : float ref *)


in
!r
end : int (* using float ref <: int ref *)

Clearly, this is incorrect and violates preservation.


Conversely if we assume subtyping is covariant, that is, τ ref ≤ σ ref if
τ ≤ σ, then

let val r = ref 3 (* r : int ref *)


in
r := 2.1; (* using int ref <: float ref *)
!r
end : int

To avoid these counterexamples we make mutable references non-variant.


τ ≤σ σ≤τ
τ ref ≤ σ ref

More detailed analyses of references are possible. In particular, we can


decompose them into “sources” from which we can only read and “sinks”
to which we can only write. Sources are covariant and sinks are contravari-
ant. Since we can both read from and write to mutable references, they
must be non-variant. We will not develop this formally here.
Note that non-variance of references is an important issue in object-
oriented languages. For example, in Java every element of an array acts

L ECTURE N OTES O CTOBER 19, 2004


Subtyping L14.7

like a reference and should therefore be non-variant. However, in Java, ar-


rays are co-variant, so run-time checks on types of assigments to arrays or
mutable fields are necessary in order to save type preservation. In particu-
lar, every time one writes to an array of objects in Java, a dynamic tag-check
is required, because arrays are co-variant in the element type. Fortunately,
this is possible because in Java and other object-oriented languages there
is enough information at run-time to perform this check reasonably effi-
ciently.
There are other type constructors that must be non-variant. For exam-
ple, if we define (in ML)

datatype ’a func = F of ’a -> ’a

then the rule


τ ≤σ σ≤τ
τ func ≤ σ func
is forced by the occurrence of ’a in both a co-variant and contra-variant
position in the argument to the constructor F.
Conversely, a type such as

datatype ’a singleton = Unit of unit

has only a single element, Unit, independently of the instantiation of ’a.


Therefore we can pose, for arbitrary τ and σ, that

τ singleton ≤ σ singleton

without fear of compromising soundness.

L ECTURE N OTES O CTOBER 19, 2004

You might also like