Princeton University
Computer Science Dept.

Computer Science 441
Programming Languages
Fall 1998

Lecture 13


Type Systems

Languages like Pascal, C, Modula-2, etc. have monomorphic type systems. Each legal expression has exactly one type.

In Pascal get extreme position that procedures only take arrays w/exactly same dimensions as well as types.

Modula-2 starts opening up, by allowing different sizes of arrays as parameters, but still require same base type.

Restrictive since sort procedures will work with any ordered type.

Rather work with "polymorphic" functions and procedures.

Two main flavors of polymorphism: ad hoc and parametric (talk about subtype later).

Ad hoc also known as overloading. Done well it can make code easier to understand. E.g., "+" for real and integer addition.

Done poorly it can confuse and lead to errors, e.g. "+" for string concatenation.

Most languages provide some overloading (e.g., arith. operators, I/O), a few (e.g., Ada and C++) allow user to define overloaded operations (either infix or prefix).

E.g. in Ada, write

function "+"(m,n:Complex) return Complex is begin ... end;

Must provide a mechanism to support resolution of overloading - i.e. which actual operation does an instance refer to.

Two major ways of resolving:

  1. Context-independent: Resolution based solely on type of arguments (Pascal, ML, C++). E.g., m + n returns complex only if m, n are both complex.

  2. Context-dependent: Resolution based on surrounding context of expression.
    E.g., in Ada can define: function "+"(m,n:real) return Complex is ... end;

    Then, in "c + (3.3 + 4.1)", inner addition will return complex if "c" is complex and will return real if "c" is real.

    Easy to write expressions which have more than one assigment of operations to overloaded operators. Termed "erroneous" and should generate error.

Overloading is not essential and is easily abused. Useful when matches expectations from external notation. Confusing and unhelpful otherwise.

Parametric polymorphism usually considered a good thing.

These are almost always related to parameterized types.

E.g., sorting with array of T where T is ordered

Push, pop with stack of T.

Search of Binary search tree of T.

In ML, direct support for polymorphic functions on lists (parameterized).

Can define parameterized types through datatype definitions.
E.g., datatype 'a stack = Empty | Push of 'a * ('a stack)

Languages support two different forms of parametric polymorphism:
implicit and explicit.

Differ on whether must supply type parameter.

Implicit Polymorphism

ML is example of implicit polymorphism. Needn't supply type of any expression. System derives most general type, often polymorphic.
E.g., reverse: 'a list -> 'a list.

Very clever type inference algorithm invented independently by Hindley and Milner.
Collects all information about types of terms in expression (assigning a variable if no info) and then tries to "unify" all information so mutually consistent.

E.g.

fun map f nil = nil
  | map f (hd::tl) = (f hd):: (map f tl)

map: 'a -> 'b -> 'c
'b = 'd list (since second arg is "nil" or "hd::tl")

f: 'a = 'd -> 'e (since "f hd" is defined)

'c = 'e list (since result is a list, and, in 2nd clause, first element is "f hd")

Therefore, map: ('d -> 'e) -> ('d list) -> ('e list)

Overloading does not interact well with implicit polymorphism because often don't have type info necessary to disambiguate expressions.

Some ML designers (e.g., Harper) feel that it would have been better to omit overloading of built-in functions.

Explicit Polymorphism

Clu, Ada, Eiffel, Modula-3, C++

No type inference. If have polymorphic function, must explicitly pass type parameter.

E.g., fun map t u (f: t -> u) (l: t list): u list

Apply: map string int length ["a","help","Willy"] = [1,4,5]

Write type as

forall t. forall u. (t -> u) -> (t list) -> u list

Makes clear that t, u are type variables.

Can understand implicit polymorphic terms as abbreviations for explicit polymorphic terms. Compare with type of map in ML, above.

Explicit polymorphic terms more expressive!

Restrictions on polymorphism for ML.

Polymorphic functions can be defined at top-level or in let clauses, but polymorphic function expressions cannot be used as arguments of functions.

E.g.,

	let fun id x = x in (id "ab", id 17) end;
is fine, but can't write
	let fun test g = (g [], g "ab")
		in test (fn x => x) end;
In fact, can't even write:
	fun test2 f = (f [], f 17);
Gets confused since f is used with two different typings:

'a list -> 'b, int -> 'c and can't unify 'a list and int.

No problem writing this in explicit polymorphic language:

    let fun test  (g: forall t.t -> t): (int list, string) = 
                                        (g (int list) [], g string "ab")
        in test (Fn t => fn (x:t) => x) end;

Write f: forall t. T, if f takes a type parameter U and returns a value of type T[U/t].
Use Fn t => to indicate parameter is a type.

Thus "id", defined by:

	fun id t (x:t) = x
has type: forall t. t -> t.

A polymorphic function is one which takes a type parameter, which specializes it into a function of particular type. I.e., id T is function of type T -> T.

Think of "id" as representing a class of functions, each of which has uniform (parameterized) type.

Subtypes

While subtyping is usually associated with object-oriented languages, it also makes sense in more traditional functional or imperative languages. We will explore subtyping before beginning to examine object-oriented languages.

When can an element of one type be used in a context which expects another?

If an element of type T can be used whereever an element of type T' is expected then say T is a subtype of T' (write T <: T'). Another way of putting this is that any element of T can masquerade as an element of type T'.

Example: Integer <: Real, 1..10 <: Integer

Note this is only true of expressions, not variables!

Can understand definition of subtype as implying existence of a (well-behaved) implicit coercion from sub to supertype.

Well-behaved = homomorphism w.r.t. operators. I.e., ToReal: Integer -> Real

	ToReal(2 + 3 * 4) = ToReal(2) + ToReal(3) * ToReal(4)
Ada provides subranges of all primitive types (including reals): always inherit operations of base class.

Get subtypes of open array type by freezing bounds.

Get subtype of variant record by freezing variant.

Are there other reasonable subtypes?

Suppose have

	type point = {x, y: real};
	datatype colortype = Red | Blue | Yellow | Green;
	type colorpoint = {x,y : real; color: colortype}

Is colorpoint <: point?

Suppose have

	fun dist(p:point):real = sqrt(sqr(p.x) + sqr(p.y));
Is there any reason this cannot be applied to cp:colorpoint?

Record Types

What are the general rules for when one record type can masquerade as another?

The type of a record includes both the labels and types of each field. Thus a record with labels li of type Ti for 1 <= i <= n has a type which can be written in the form {li:Ti}1 <= i <= n. For example, the record {name = ``jane'',age = 12} has type {name: String; age: Integer}.

Suppose we have two immutable (i.e., fields can't be updated) record types, R' and R, with R' <: R. In order for elements of R' to masquerade as elements of R, expressions of type R' need to support all of the operations applicable to expressions of type R.

The only operation available on a record is to extract a labeled field.

For ${\it {R'}}$ to masquerade as ${\it {R}}$,then, for each field ${\it {l}}$ of ${\it {R}}$, ${\it {R'}}$ must also contain a field ${\it {l}}$. Moreover the type of that field must be a subtype of the corresponding field in ${\it {R}}$, since if ${\it {r':\ R'}}$ then ${\it {r'.l}}$must be usable in any context in which the same field selection of an element of type ${\it {R}}$ makes sense. In Figure 1 we show a record ${\it {r':\ \{l_1:T'_1;l_2:T'_2;l_3:T'_3;l_4:T'_4\}}}$ masquerading as a record of type ${\it {\{l_1:T_1;l_2:T_2;l_3:T_3\}}}$. Notice that the result of extracting the ${\it {l_i}}$ field of ${\it {r'}}$ must be able to be treated as being of type ${\it {T_i}}$.Notice also that type ${\it {R'}}$ may have more labeled fields than ${\it {R}}$ (since the extra fields don't get in the way of any of the operations applicable to ${\it {R}}$).


  
Figure 1: A record ${\it {r:\ \{l_1:T_1;l_2:T_2;l_3:T_3\}}}$, and ${\it {r':\ \{l_1:T'_1;l_2:T'_2;l_3:T'_3;l_4:T'_4\}}}$ masquerading as an element of type ${\it {\{l_1:T_1;l_2:T_2;l_3:T_3\}}}$.
\begin{figure}
\begin{center}
\begin{picture}
(350,90)

\thicklines 
 
\put(00,0...
 ...(330,30){\makebox(0,0){{\rm${\it {T_3}}$}}}\end{picture}\end{center}\end{figure}

Thus a subtype of a record may have labeled fields whose types are a subtype of the original, and may also have more fields than the original record type. We write this formally as follows:

\begin{displaymath}
\{ {\rm{\it { l_j:T_j'}}}\}_{1 \le j \le n} {\rm \ <: \ }\{ ...
 ... \le i \le k, {\rm{\it {T_i'}}} {\rm \ <: \ }
{\rm{\it {T_i}}}.\end{displaymath}

For example, if ${\it {TropicalFruit {\rm \ <: \ }Fruit}}$, then

\begin{displaymath}
{\rm{\it {\{main: MainDish; dessert: TropicalFruit; drink: Beverage\} {\rm \ <: \ }
\{main: MainDish; dessert: Fruit\}}}}.\end{displaymath}

Function types

We write Func(S): T for the type of functions that take a parameter of type ${\it {S}}$ and return a result of type ${\it {T}}$. If ${\it {{\it Func(S'):\ T'} {\rm \ <: \ }{\it Func(S):\ T}}}$, then we should be able to use an element of the first functional type in any context in which an element of the second type would type check.


  
Figure 2: A function ${\it {f:\ {\it Func(S):\ T}}}$, and ${\it {f':\ {\it Func(S'):\ T'}}}$ masquerading as ${\it {f}}$.
\begin{figure}
\begin{center}
\begin{picture}
(250,110)

\thicklines 
 

\put(5,...
 ...ut(240,20){\makebox(0,0){{\rm${\it {T}}$}}}\end{picture}\end{center}\end{figure}

Suppose we have a function ${\it {f}}$ with type Func(S): T. In order to use an element, ${\it {f'}}$, of type ${\it Func(S'):\ T'}$ in place of ${\it {f}}$, the function ${\it {f'}}$ must be able to accept an argument of type ${\it {S}}$ and return a value of type ${\it {T}}$. See Figure 2. But ${\it {f'}}$ is defined to accept arguments of type ${\it {S'}}$. Now ${\it {f'}}$ can be applied to an argument, ${\it {s}}$, of type ${\it {S}}$ if ${\it {S {\rm \ <: \ }S'}}$. In that case, using subsumption, ${\it {s}}$ can be treated as an element of type ${\it {S'}}$, making ${\it {f'(s)}}$ typable. Similarly, if the output of ${\it {f'}}$ has type ${\it {T'}}$ then ${\it {T' {\rm \ <: \ }T}}$ will guarantee that the output can be treated as an element of type ${\it {T}}$. Summarizing,

\begin{displaymath}
{\it Func(S'):\ T'} {\rm \ <: \ }{\it Func(S):\ T} \mbox{ if...
 ...S'}}}
\mbox{ and } {\rm{\it {T'}}} {\rm \ <: \ }{\rm{\it {T}}}.\end{displaymath}

Again assuming that ${\it {TropicalFruit {\rm \ <: \ }Fruit}}$, we get that

\begin{displaymath}
{\rm{\it {{\it Func(Integer):\ TropicalFruit} {\rm \ <: \ }{\it Func(Integer):\ Fruit}}}},\end{displaymath}

but

\begin{displaymath}
{\rm{\it {{\it Func(Fruit):\ Integer} {\rm \ <: \ }{\it Func(TropicalFruit):\ Integer}}}}.\end{displaymath}

Procedure types, written ${\it {Proc(S)}}$, may be subtyped as though they were degenerate function types that always return a default type ${\it {
Unit}}$, which has only a single value. Thus

\begin{displaymath}
Proc(S') {\rm \ <: \ }Proc(S) \mbox{ if } {\rm{\it {S}}} {\rm \ <: \ }{\rm{\it {S'}}}.\end{displaymath}

Notice that the ordering of parameter types in function and procedure subtyping is the reverse of what might initially have been expected, while the output types of functions are ordered in the expected way. We say that subtyping for parameter types is contravariant (i.e., goes the opposite direction of the relation being proved) while the subtyping for result types of functions is covariant (i.e., goes in the same direction). The contravariance for parameter types can be initially confusing, because it is permissible to replace an actual parameter by another whose type is a subtype of the original. However the key is that in the subtyping rule for function types it is the function, not the actual parameter, which is being replaced.

Types of variables

Variables of type ${\it {T}}$ have different properties from ordinary expressions of type ${\it {T}}$, because variables may be assigned to. We call the types of variables reference types, and say that a variable declared to have type ${\it {T}}$ actually has type ${\it {{\rm{\it ref}}\ T}}$.[*] This name is reasonable since a variable of type ${\it {T}}$ actually denotes a location (or reference) in which one can store a value of type ${\it {T}}$. The ability to assign a value to a variable will have a great impact on the subtype properties (or rather the lack of them) of reference types.

Suppose we have a variable ${\it {x'}}$ of type ${\it {T'}}$ (i.e., an expression of type ${\it {{\rm{\it ref}}\ T'}}$) that we wish to have masquerade as a variable of type ${\it {T}}$. See Figure 3. It is common to think of a variable of type ${\it {T}}$ as having two values: an L-value and an R-value. The L-value is the location corresponding to the variable, while the R-value is the value of type ${\it {T}}$ actually stored there.

Variables can be used in both value-supplying and value-receiving contexts. A value-supplying context is one that requires a value of type ${\it {T}}$ (i.e., the R-value of the variable). This is illustrated in the figure by the operation (arrow) labeled ``val'' coming out of the variable (representing the R-value of the variable). By the definition of subtype, in order for a variable ${\it {x'}}$ of type ${\it {T'}}$ to be able to masquerade as a value of type ${\it {T}}$ in all contexts of this kind, we need ${\it {T' {\rm \ <: \ }T}}$. This should be clear from the right-hand diagram in the figure, where in order for ${\it {x'}}$ to provide a compatible value using the ${\it {val}}$ operator, ${\it {T' {\rm \ <: \ }T}}$.

A value-receiving context is one in which a variable of type ${\it {T}}$ is assigned to, e.g., a statement of the form ${\it {x := e}}$, for ${\it {e}}$ an expression of type ${\it {T}}$. This is illustrated in the figure by an arrow labeled ``:='' going into the variable. In this context we will be interpreting the variable as a reference or location (i.e., the L-value) in which to store a value. We have already seen above that an assignment like this is type safe if ${\it {e}}$ has a type that is a subtype of the type of the variable ${\it {x}}$. Thus if we wish to use a variable of type ${\it {T'}}$ in all contexts of this form, we must ensure that ${\it {T {\rm \ <: \ }T'}}$. Again this should be clear from the right-hand diagram in the figure.

Thus for a variable of type ${\it {T'}}$ to masquerade as a variable of type ${\it {T}}$ in value-supplying contexts we must have ${\it {T' {\rm \ <: \ }T}}$, while its use in value-receiving contexts require ${\it {T {\rm \ <: \ }T'}}$. It follows that there are no non-trivial subtypes of variable (reference) types. Thus,

\begin{displaymath}
{\rm{\it {{\rm{\it ref}}\ T'}}} {\rm \ <: \ }{\rm{\it {{\rm{...
 ...ref}}\ T}}} \mbox{ iff } {\rm{\it {T'}}} \simeq {\rm{\it {T}}},\end{displaymath}

where ${\it {T' \simeq T}}$ abbreviates ${\it {T' {\rm \ <: \ }T}}$ and ${\it {T {\rm \ <: \ }T'}}$. We can think of $\simeq$ as defining an equivalence class of types including such things as pairs of record types that differ only in the order of fields. It is common to ignore the differences between such types and to consider them equivalent.


  
Figure 3: A variable ${\it {x:\ ref\ T}}$, and ${\it {x':\ ref\ T'}}$ masquerading as ${\it {x}}$.
\begin{figure}
\begin{center}
\begin{picture}
(350,60)

\thicklines 
 
\put(00,0...
 ...ut(330,20){\makebox(0,0){{\rm${\it {T}}$}}}\end{picture}\end{center}\end{figure}

Another way of understanding the behavior of reference and function types under subtyping is to consider the different roles played by suppliers and receivers of values. Any slot in a type expression that corresponds to a supplier of values must have subtyping behave covariantly (the same direction as the full type expression), while any slot corresponding to a receiver of values must behave contravariantly (the opposite direction). Thus L-values of variables and parameters of functions, both of which are receivers of argument values, behave contravariantly with respect to subtyping. On the other hand, the R-values of variables and the results of functions, both of which are suppliers of values, behave covariantly. Because variables have both behaviors, any changes in type must be simultaneously contravariant and covariant. Hence subtypes of reference types must actually be equivalent.

The subtyping relation among object types can be rather subtle. In simple type systems like those used in Java and C++, object types are considered subtypes when the record of method types of the subtype extends that of the supertype. That is, a subtype may contain more methods than the supertype, but the corresponding methods must have the same types. This is more restrictive than the definition of subtype for record types given above. We will see later that there is no need for such a restrictive notion of subtyping.


CS441 | CS Department | Princeton University