is applied to an argument, then a constraint equating the domain type of the function with the type of the argument is generated. Second, the con- straints are solved using a process similar to Gaussian elimination, called
unification. The equations can be classified by their solution sets as follows: 1. Overconstrained: there is no solution. This corresponds to a type error. 2. Underconstrained: there are many solutions. There are two sub-cases:
ambiguous(due to overloading, which we will discuss further insec- tion 8.3), orpolymorphic(there is a “best” solution).
3. Uniquely determined:there is precisely one solution. This corresponds to a completely unambiguous type inference problem.
The free type variables in the solution to the system of equations may be thought of as determining the “degrees of freedom” or “range of polymor- phism” of the type of an expression — the constraints are solvable for any choice of types to substitute for these free type variables.
This description of type inference as a constraint satisfaction procedure accounts for the notorious obscurity of type checking errors in ML. If a program is not type correct, then the system of constraints associated with it will not have a solution. The type inference procedure attempts to find a solution to these constraints, and at some point discovers that it cannot succeed. It is fundamentally impossible to attribute this inconsistency to any particular constraint; all that can be said is that the constraint set as a whole has no solution. The checker usually reports the first unsatisfi- able equation it encounters, but this may or may not correspond to the “reason” (in the mind of the programmer) for the type error. The usual method for finding the error is to insert sufficient type information to nar- row down the source of the inconsistency until the source of the difficulty is uncovered.
8.2
Polymorphic Definitions
There is an important interaction between polymorphic expressions and value bindings that may be illustrated by the following example. Suppose that we wish to bind the identity function to a variable Iso that we may refer to it by name. We’ve previously observed that the identity function
8.2 Polymorphic Definitions 71
is polymorphic, with principal type scheme’a->’a. This may be captured by ascribing this type scheme to the variableIat thevalbinding. That is, we may write
val I : ’a->’a = fn x=>x
to ascribe the type scheme’a->’ato the variableI. (We may also write fun I(x:’a):’a = x
for an equivalent binding ofI.) Having done this,each use ofIdetermines a distinct instance of the ascribed type scheme’a->’a. That is, bothI 0andI I 0 are well-formed expressions, the first assigning the typeint->inttoI, the second assigning the types
(int->int)->(int->int) and
int->int
to the two occurrences ofI. Thus the variableIbehaves precisely the same as its definition,fn x=>x, in any expression where it is used.
As a convenience ML also provides a form of type inference on value bindings that eliminates the need to ascribe a type scheme to the variable when it is bound. If no type is ascribed to a variable introduced by a valbinding, then it is implicitly ascribed the principal type scheme of the right-hand side. For example, we may write
val I = fn x=>x or
fun I(x) = x
as a binding for the variable . The type checker implicitly assigns the prin- cipal type scheme, ’a->’a, of the binding to the variableI. In practice we often allow the type checker to infer the principal type of a variable, but it is often good form to explicitly indicate the intended type as a consistency check and for documentation purposes.
The treatment of val bindings during type checking ensures that a bound variable has precisely the same type as its binding. In other words
8.2 Polymorphic Definitions 72
the type checker behaves as though all uses of the bound variable are im- plicitly replaced by its binding before type checking. Since this may in- volve replication of the binding, the meaning of a program is not necessar- ily preserved by this transformation. (Think, for example, of any expres- sion that opens a window on your screen: if you replicate the expression and evaluate it twice, it will open two windows. This is not the same as evaluating it only once, which results in one window.)
To ensure semantic consistency, variables introduced by avalbinding are allowed to be polymorphiconly if the right-hand side is a value. This is called the value restrictionon polymorphic declarations. For fun bind- ings this restriction is always met since the right-hand side is implicitly a lambda expression, which is a value. However, it might be thought that the following declaration introduces a polymorphic variable of type’a -> ’a, but in fact it is rejected by the compiler:
val J = I I
The reason is that the right-hand side is not a value; it requires compu- tation to determine its value. It is therefore ruled out as inadmissible for polymorphism; the variableJmay not be used polymorphically in the re- mainder of the program. In this case the difficulty may be avoided by writing instead
fun J x = I I x
because now the binding ofJis a lambda, which is a value.
In some rare circumstances this is not possible, and some polymor- phism is lost. For example, the following declaration of a value of list type1
val l = nil @ nil
does not introduce an identifier with a polymorphic type, even though the almost equivalent declaration
val l = nil
does do so. Since the right-hand side is a list, we cannot apply the “trick” of defininglto be a function; we are stuck with a loss of polymorphism in
1To be introduced inchapter 9.