II. Co-contextual Type Checkers for Functional Languages
3. Co-contextual PCF
5.3. Co-Contextual Constraints for Let-Polymorphism
expression containing variables. For example, when encountering the application x 1 the type of x is yet unknown and may be a ground type or a type variable. Therefore, the type schema we introduce is constructed from a type and the list of all unification variables of the type under scrutiny. This list represents unification variables that are placeholders for 1) type variables, or 2) ground types. In the latter case, those unification variables are removed from the list since they do not represent a polymorphic type.
Let us consider the above example. The co-contextual type checker starts at the leaves of the syntax tree and, while moving up the tree, gathers more information about the used variables. When encountering the declaration of y, the type checker does not know the type of x. It is known that x is applied to a number but the return type is unknown. Therefore, the type of x is N um → U0, where U0 is a fresh unification variable. The resulting type of the application is U0. Therefore, the generalized type of y is T Schema(U0, List(U0)). When encountering the let binding of x, we learn that x is of
type U → U . Thus, the type of U0 is actually a number (ground type). But at the point of type checking the let binding of y, this information is not yet provided. Therefore, unification variables are considered as type variables until proven otherwise.
Unification schemas are the second construct introduced for the co-contextual type checker. Since the type checker has no information on the actual types of the program variables used in expressions, we do not know their actual partial type schemas. For example, while type checking the body of the inner let from the example above, we do not know the actual type of the variable y. Therefore, we associate y to the so-called unification schema (U S), as placeholder for its actual type, which is a partial type schema. We assume that all variables are program variables until proven otherwise. The U S serves as placeholder for both cases, when a type is a partial type schema, or a non-polymorphic type. When encountering the let binding of y, these unification schemas will be resolved to a partial type schema (U S = T Schema(U0, List(U0))).
In summary, we have introduced two new types, partial type schemas consisting of unification variables and unification schemas. Unification variables serve as placeholders only for non-polymorphic types while unification schemas can be used as placeholders for all types. Both constructs are necessary in the co-contextual type system and their usage during the type checking is discussed in the following section, where we will describe the co-contextual constraints of let-polymorphism in detail.
5.3. Co-Contextual Constraints for Let-Polymorphism
Previously, we talked about partial type schemas, but how are they generated and later instantiated in the body of let? We answer this question in the following of this section. Similar to SML, we first generalize the type of bound expression, which we assign to the program variable, and then instantiate the type of the program variable for all its usages in the let body. In order to perform these two steps in the co-contextual type system, we introduce partial generalization constraints and instantiation constraints. Both of these constraints are discussed in detail in the following subsections. In addition, we
introduce substitution constraints and compatibility constraints. A substitution constraint replaces unification variables U , which occur in a type to other unification variables. These constraints are similar to the constraints introduced by parametric polymorphism in the previous Chapter 4 and, hence, are not discussed in more detail in this chapter. Compatibility constraints are used to correlate the program variable with its instances from the let body. Their usage is tied to the instantiation and both are described in detail in this section. In summary the set of constraints is extended as follows:
c ::= . . .
| T = GenP (T ) partial generalization constraint | T = Inst(T ) instantiation constraint
| T = {U 7→U }T substitution constraint
| T ∼ T compatibility constraint
5.3.1. Partial Generalization Constraint
As dual to the generalization function schema found in the contextual let-polymorphism, we introduce partial generalization constraints using the function GenP that generates partial type schemas. Unification variables of a type T are used as part of the list (List[U ]) of the partial type schema, which is generated from generalizing T , as explained in previous section. We introduce an auxiliary function to extract the unification variables from a type, then perform the partial generalization to obtain a partial type schema. This function is called occurU V ar and is shown below:
occurUVar(t : Type): List[U] = { t match {
case Num => List() case U => List(U)
case t1 -> t2 = occurUVar(t1) ++ occurUVar(t2) case _ => List()
} }
We distinguish different cases for the type we want to generalize. First, if the type is a ground type, i.e., a number, then it does not have unification variables. Second, if the type is a unification variable, then it is added to the list. Finally if a type is a function type then we call the function recursively on both the argument type t1 and the result type t2. As a result the partial generalization function for obtaining partial type schemas is:
GenP (T ) = T Schema(T, occurU V ar(T ))
Note that a schema generated from a ground type is considered to be ground, such that T Schema(T, List()) = T .
5.3. Co-Contextual Constraints for Let-Polymorphism
5.3.2. Instantiation Constraint
Instantiation constraints (T = Inst(T )) are the dual to the instantiate function described for contextual let-polymorphism. Inst is the partial instantiation function, which generates instances of co-contextual types. This function is applied to all let types, including the partial type schemas, as shown below:
Inst(T ) = T if T is ground U if T = U Inst(T ) → Inst(T ) if T = T → T
{U 7→U0}T where U0 are f resh if T = T Schema(T, U )
Inst(U S) if T = U S
(5.1)
We distinguish five cases for partial instantiation. Ground types are already instances and the instantiation is the ground type itself. Likewise, unification variables are instances (in the let formalism) and instantiation is the unification variable itself. Function types recursively instantiate their parameter type and return type. Partial type schemas are instantiated by generating fresh unification variables for all usages of the program variable in the body of let. Finally, unification schemas cannot be instantiated, since they are only placeholders for yet to be determined types, i.e., all types in the previous four cases of instantiation are possible.
In contrast to the contextual instantiate function, the co-contextual type checking does not instantiate type variables but unification variables, since it is bottom-up with no information on the program variable’s type. Instead we generate fresh unification variables, which correspond to the unification variables of a partial type schema. These unification variables are potential type variables or simply ground types.
The concept of instantiation is very challenging, since the type information must be correlated between the types of the instances encountered in the let body and type of the program variable in the let binding. The relation between the instances (fresh unification variables) and the instantiated type (unification variables that are part of the partial type schema) needs to be captured in the constraint system via compatibility constraints.
For example, let us consider the partial type schema of the program variable y from the example above (T Schema(U0, List(U0))). y is used two times in the body of let (+ y y), therefore there will be two instances of y. Each of the instances is associated to a fresh unification variable: U1 and U2, respectively. Both U1 and U2 have type N um due to being operands of +. Let us now consider the let binding of y, which was associated with the unification variable U0. U0 is the result type of the function application of x to the number 1. The program variable x represents an identity function with the type U → U . Therefore, applying the identity function to a number gives as a result a number. Consequently, we get that U0 is also a number (U0 = N um). Since U0 is a number, which is ground type, then its instances must be of the same ground type. The program is well-typed because U1 and U2 are also numbers, but the relation between U0, U1, and U2 needs to be captured for this to be checked. If we consider a different program
instead, where x is applied to a character0a of type Char, i.e., y = x0a, the type of U0 is Char. However, the instances of y are of type N um and the program is not well-typed (N um = Char). Without further constraints, U0, U1, and U2 are distinct unification variables, with no connection among each other, and the type error would not be found. We propose to generate compatibility constraints (of the form T ∼ T ) to correlate the program variable and its instances. These constraints are generated when encountering an instantiation constraint on a partial type schema. That is, when more information on the types is provided and the unification schemas are resolved to partial type schemas.
We show when this relation is generated and how the compatibility constraints operate in the following:
Assumption 1. Given a set of constraints C and (T = Inst(T Schema(T0, List(U )))) ∈ C, then applying Inst yields T = {U 7→U0}T and C is updated to C ∪ {U ∼ U0}, where U0 are fresh unification variables.
Compatibility constraints will be translated into equality constraints when additional type information about the program variable is provided. Given a program variable of type T0 and instances in the let body of types T1, . . . , Tn, then the set of compatibility constraints is of the form {T0∼ T1. . . T0 ∼ Tn} and the following rules apply:
T0∼ T1. . . T0 ∼ Tn T0 is ground T0 = T1. . . T0= Tn
T0∼ T1. . . T0 ∼ Tn U f resh T0 is not ground T1 = U1. . . Tn= Un
We distinguish two cases for the type T0: 1) it is a ground type, or 2) a type variable (a unification variable is considered to be a type variable if we do not find a substitution for it). In the first case, all its instances should be of the same ground type. To ensure this, equality constraints are generated, i.e., T0 = T1. . .. In the second case, a type variable can have instances of different types and these should not be equal to each other. Hence, we generate equality constraints between fresh unification variables and the instances of T0 (one for each instance).
Let us consider the instantiations of the program variable y with type T Schema(U0, List(U0)). Instantiating a partial type schema generates compatibility
constraints. As described above U0 is instantiated two times U1 and U2. Therefore, two compatibility constraints are added, i.e., U0 ∼ U1 and U0∼ U2.
For example in the initial expression where x is applied to a number, the unification variable U0 is also a number. Consequently, compatibility constraints are turned to equality constraints indicating that also U1 and U2 must be numbers, i.e., U0 = U1, U0 = U2 and we know that U0 = N um, therefore, U1 = N um and U2 = N um. In
contrast, when x is applied to a Char, then also U0= Char. Therefore, the compatibility constraints (U0∼ U1 and U0∼ U2) are turned to equality constraints indicating that U1
5.4. Co-Contextual Let-Polymorphism by Example