Hindley–Milner
In type theory, Hindley–Milner (HM) (also known as Damas–Milner or Damas–Hindley–Milner) is a classical type inference method with parametric polymorphism for the lambda calculus, first described by J. Roger Hindley^{[1]} and later rediscovered by Robin Milner.^{[2]} Luis Damas contributed a close formal analysis and proof of the method in his PhD thesis.^{[3]}^{[4]}
Among the properties making HM so outstanding is completeness and its ability to deduce the most general type of a given source without the need of any type annotations or other hints. HM is a fast algorithm, computing a type almost in linear time with respect to the size of the source, making it practically usable to type large programs. HM is preferably used for functional languages. It was first implemented as part of the type system of the programming language ML. Since then, HM has been extended in various ways, most notably by constrained types as used in Haskell.
Introduction
Organizing their original paper, Damas and Milner^{[4]} clearly separated two very different tasks. One is to describe what types an expression can have and another to present an algorithm actually computing a type. Keeping both aspects apart from each other allows to focus separately on the logic (i.e. meaning) behind the algorithm, as well as to establish a benchmark for the algorithm's properties.
How expressions and types fit to each other is described by means of a deductive system. Like any proof system, it allows different ways to come to a conclusion and since one and the same expression arguably might have different types, dissimilar conclusions about an expressions are possible. Contrary to this, the type inference method itself (Algorithm W) is defined as a step-by-step procedure, leaving no choice what to do next. Thus clearly, decisions not present in the logic might have been made constructing the algorithm, which demand a closer look and justifications but would perhaps remain non-obvious without the above differentiation.
Syntax
Expressions |
Types |
Logic and algorithm share the notions of "expression" and "type", whose form is made precise by the syntax.
The expressions to be typed are exactly those of the lambda calculus, enhanced by a let-expression.
Readers unfamiliar with the lambda calculus might not only be puzzled by the syntax, which can quickly be straightened out translating, that the application represents the function application, often written and that the abstraction means anonymous function or function literal, common in most contemporary programming languages, there perhaps spelled only more verbosely .
Types as a whole are split into two groups, called mono- and polytypes.^{[note 1]}
Monotypes , syntactically terms, always designate a particular type in the sense, that it is equal only to itself and different from all others. The most typical representatives of monotypes are type constants like or . Types can be parametric like . All these types are examples of applications of type functions , i.e. in the before mentioned examples, where the superscript indicates the number of type parameters. While the choice of is completely arbitrary, in context of HM it must contain at least , the type of functions, which is written infix for convenience, e.g. a function mapping integers to strings has type . ^{[note 2]}
Type variables are monotypes. Standing alone, a type variable is meant to be as concrete as or and clearly different from both. Type variables occurring as monotypes behave as if they were type constants, of which one only does not have any further information. Correspondingly, a function typed only maps values of the particular type on itself. Such a function can only be applied to values having type and to no others.
A function with polytype by contrast can map any value of the same type to itself, and the identity function is a value for this type. As another example is the type of a function mapping all finite sets to integers. The count of members is a value for this type. Note that qualifiers can only appear top level, i.e. a type for instance, is excluded by syntax of types and that monotypes are included in the polytypes, thus a type has the general form .
Free type variables
Free Type Variables |
In a type , the symbol is the qualifier binding the type variables in the monotype . The variables are called qualified and any occurrence of a qualified type variable in is called bound and all unbound type variables in are called free. Like in the lambda calculus, the notion of free and bound variables are essential for the understanding of the meaning of types.
This is certainly the hardest part of HM, perhaps because polytypes containing free variables are not represented in programming languages like Haskell. Likely, one does not have clauses with free variables in Prolog clauses. In particular developers experienced with both languages and actually knowing all the prerequisites of HM, are likely to slip this point. In Haskell for example, all type variables implicitly occur qualified, i.e. a Haskell type means here. Because a type like , though it may practically occur in a Haskell program, cannot be expressed there, it is easily be confused with its qualified version.
So what function can have a type like e.g. , i.e. a mixture of both bound and unbound type variables and what could the free type variable therein mean?
Example 1 |
Consider in Example 1, with type annotations in brackets. Its parameter is not used in the body, but the variable bound in the outer context of surely is. As a consequence, accepts every value as argument, while returning a value bound outside and with it its type. to the contrary has type , in which all occurring type variables are bound Evaluating, for instance , results in a function of type , perfectly reflecting that foo's monotype in has been refined by this call.
In this example, the free monotype variable in foo's type becomes meaningful by being qualified in the outer scope, namely in bar's type. I.e. in context of the example, the same type variable appears both bound and free in different types. As a consequence, a free type variable cannot be interpreted better than stating it is a monotype without knowing the context. Turning the statement around, in general, a typing is not meaningful without a context.
Context and typing
Syntax |
Free Type Variables |
Consequently, to get the yet disjoint parts of the syntax, expressions and types together meaningfully, a third part, the context is needed. Syntactically, it is a list of pairs , called assignments or assumptions, stating for each value variable therein a type . All three parts combined gives a typing of the form , stating, that under assumptions , the expression has type .
Now having the complete syntax at hand, one can finally make a meaningful statement about the type of in example 1, above, namely . Contrary to the above formulations, the monotype variable no longer appears unbound, i.e. meaningless, but bound in the context as the type of the value variable . The circumstance whether a type variable is bound or free in the context apparently plays a significant role for a type as part of a typing, so it is made precise in the side box.
Note on expressiveness
Since the expression syntax might appear far too inexpressive to readers unfamiliar with the lambda calculus, and because the examples given below will likely support this misconception, some notes that the HM is not dealing with toy languages might be helpful. As a central result in research on computability, the expression syntax defined above (without the let-variant) is able to express any computable function. Moreover all other programming language constructions can be relatively directly transformed syntactically into expressions of the lambda calculus. Therefore, this simple expression is used as a model for programming languages in research. A method known to work well for the lambda calculus can easily be extended to all or at least many other syntactical construction of a particular programming language using the before mentioned syntactical transformations.
As an example, the additional expression variant can be transformed to . It is added to expression syntax in HM only to support generalization during the type inference and not because syntax lacks computational strength. Thus HM deals with inference of types in programs in general and the various functional languages using this method demonstrate, how well a result formulated only for the syntax of the lambda calculus can be extend to syntactically complex languages.
Contrary to the impression, that the expressions might be too inexpressive for practical application, they are actually far too expressive to be meaningfully typed at all. This is a consequence of the decision problem being undecidable for anything as expressive as the expression of the lambda calculus. Consequently, computing typings is a hopeless venture in general. Depending on the nature of the type system, it will either never terminate or otherwise refuse to work.
HM belongs to the later group of type systems. A collapse of the type system presents itself then as more subtle situation in that suddenly only one and the same type is yielded for the expressions of interest. This is not a fault in HM, but inherent in the problem of typing itself and can easily be created within any strongly typed programming language e.g. by coding an evaluator (the universal function) for the "too simple" expression. One then has a single concrete type that represents the universal data type as usual in untyped languages. The type system of the host programming language is then collapsed and cannot longer differentiate between the various types of values handed to or produced by the evaluator. In this context, it still delivers or checks types, but always the same, just as if the type system were not longer present at all.
Polymorphic type order
While the equality of monotypes is purely syntactical, polytypes offer a richer structure by being related to other types through a specialization relation expressing that is more special than .
When being applied to a value a polymorphic function has to change its shape specializing to deal with this particular type of values. During this process, it also changes its type to match that of the parameter. If for instance the identity function having type is to be applied on a number having type , both simply cannot work together, because all the types are different and nothing fits. What is needed is a function of type . Thus, during application, the polymorphic identity is specialized to a monomorphic version of itself. In terms of the specialization relation, one writes
Now the shape shifting of polymorphic values is not fully arbitrary but rather limited by their pristine polytype. Following what has happened in the example one could paraphrase the rule of specialization, saying, a polymorphic type is specialized by consistently replacing each occurrence of in and dropping the qualifier. While this rule works well for any monotype uses as replacement, it fails when a polytype, say is tried as a replacement, resulting in the non-syntactical type . But not only that. Even if a type with nested qualified types would be allowed in the syntax, the result of the substitution would not longer preserve the property of the pristine type, in which both the parameter and the result of the function have the same type, which are now only seemingly equal because both subtypes became independent from each other allowing to specialize the parameter and the result with different types resulting in, e.g. , hardly the right task for an identity function.
The syntactic restriction to allow qualification only top-level is imposed to prevent generalization while specializing. Instead of , the more special type must be produced in this case.
One could undo the former specialization by specializing on some value of type again. In terms of the relation one gains as a summary, meaning that syntactically different polytypes are equal w.r.t. to renaming their qualified variables.
Specialization Rule |
Now focusing only on the question whether a type is more special than another and not longer what the specialized type is used for, one could summarize the specialization as in the box above. Paraphrasing it clockwise, a type is specialized by consistently replacing any of the qualified variables by arbitrary monotypes gaining a monotype . Finally, type variables in not occurring free in the pristine type can optionally be qualified.
Thus the specialization rules makes sure that no free variable, i.e. monotype in the pristine type becomes unintentionally bound by a qualifier, but originally qualified variable can be replaced with whatever, even with types introducing new qualified or unqualified type variables.
Starting with a polytype , the specialization could either replace the body by another qualified variable, actually a rename or by some type constant (including the function type) which may or may not have parameters filled either with monotypes or qualified type variables. Once a qualified variable is replaced by a type application, this specialization cannot be undone through another substitution as it was possible for qualified variables. Thus the type application is there to stay. Only if it contains another qualified type variable, the specialization could continue further replacing for it.
So the specialization introduces no further equivalence on polytype beside the already known renaming. Polytypes are syntactically equal up to renaming their qualified variables. The equality of types is a reflexive, antisymmetric and transitive relation and the remaining specializations of polytypes are transitive and with this the relation is an order.
Deductive system
The Syntax of Rules |
The syntax of HM is carried forward to the syntax of the inference rules that form the body of the formal system, by using the typings as judgments. Each of the rules define what conclusion could be drawn from what premises. Additionally to the judgments, some extra conditions introduced above might be used as premises, too.
A proof using the rules is a sequence of judgments such that all premises are listed before a conclusion. Please see the Examples 2, 3 below for a possible format of proofs. From left to right, each line shows the conclusion, the of the rule applied and the premises, either by referring to an earlier line (number) if the premise is a judgment or by making the predicate explicit.
Typing rules
{{#invoke:see also|seealso}}
Declarative Rule System |
The side box shows the deduction rules of the HM type system. One can roughly divide them into two groups:
The first four rules , , and are centered around the syntax, presenting one rule for each of the expression forms. Their meaning is pretty obvious at the first glance, as they decompose each expression, prove their sub-expressions and finally combine the individual types found in the premises to the type in the conclusion.
The second group is formed by the remaining two rules and . They handle specialization and generalization of types. While the rule should be clear from the section on specialization above, complements the former, working in the opposite direction. It allow generalization, i.e. to qualify monotype variables that are not bound in the context. The necessity of this restriction is introduced the section on free type variables.
The following two examples exercise the rule system in action
Example 2: A proof for where , could be written
Example 3: To demonstrate generalization, is shown below:
Principal type
As mentioned in the introduction, the rules allow to deduce different types for one and the same expression. See for instance, Example 2, steps 1,2 and Example 3, steps 2,3 for three different typings of the same expression. Clearly, the different results are not fully unrelated, but connected by the type order. It is an important property of the rule system and this order that whenever more but one type can be deduced for an expression, among them is (modulo alpha-renaming of the type variables) a unique most general type in the sense, that all others are specialization of it. Though the rule system must allow to derive specialized types, a type inference algorithm should deliver this most general or principal type as its result.
Let-polymorphism
Not visible immediately, the rule set encodes a regulation under which circumstances a type might be generalized or not by a slightly varying use of mono- and polytypes in the rules and .
In rule , the value variable of the parameter of the function is added to the context with a monomorphic type through the premise , while in the rule , the variable enters the environment in polymorphic form . Though in both cases the presence of x in the context prevents the use of the generalisation rule for any monotype variable in the assignment, this regulation forces the parameter x in a -expression to remain monomorphic, while in a let-expression, the variable could already be introduced polymorphic, making specializations possible.
As a consequence of this regulation, no type can be inferred for since the parameter is in a monomorphic position, while yields a type , because has been introduced in a let-expression and is treated polymorphic therefore. Note that this behaviour is in strong contrast to the usual definition and the reason why the let-expression appears in the syntax at all. This distinction is called let-polymorphism or let generalization and is a conception owed to HM.
Towards an algorithm
Now that the deduction system of HM is at hand, one could present an algorithm and validate it w.r.t. the rules. Alternatively, it might be possible to derive it by taking a closer look on how the rules interact and proof are formed. This is done in the remainder of this article focusing on the possible decisions one can make while proving a typing.
Degrees of freedom choosing the rules
Isolating the points in a proof, where no decision is possible at all, the first group of rules centered around the syntax leaves no choice since to each syntactical rule corresponds a unique typing rule, which determines a part of the proof, while between the conclusion and the premises of these fixed parts chains of and could occur. Such a chain could also exist between the conclusion of the proof and the rule for topmost expression. All proof must have the so sketched shape.
Because the only choice in a proof with respect of rule selection are the and chains, the form of the proof suggests the question whether it can be made more precise, where these chains might be needed. This is in fact possible and leads to a variant of the rules system with no such rules.
Syntax-directed rule system
Syntactical Rule System |
Generalization |
A contemporary treatment of HM uses a purely syntax-directed rule system due to Clement^{[5]} as an intermediate step. In this system, the specialization is located directly after the original rule and merged into it, while the generalization becomes part of the rule. There the generalization is also determined to always produce the most general type by introducing the function , which qualifies all monotype variables not bound in .
Formally, to validate, that this new rule system is equivalent to the original , one has to show that , which falls apart into two sub-proofs:
While consistency can be seen by decomposing the rules and of into proofs in , it is likely visible that is incomplete, as one cannot show in , for instance, but only . An only slightly weaker version of completeness is provable ^{[6]} though, namely
implying, one can derive the principal type for an expression in allowing to generalize the proof in the end.
Comparing and note that only monotypes appear in the judgments of all rules, now.
Degrees of freedom instantiating the rules
Within the rules themselves, assuming a given expression, one is free to pick the instances for (rule) variables not occurring in this expression. These are the instances for the type variable in the rules. Working towards finding the most general type, this choice can be limited to picking suitable types for in and . The decision of a suitable choice cannot be made locally, but its quality becomes apparent in the premises of , the only rule, in which two different types, namely the function's formal and actual parameter type have to come together as one.
Therefore, the general strategy for finding a proof would be to make the most general assumption () for in and to refine this and the choice to be made in until all side conditions imposed by the rules are finally met. Fortunately, no trial and error is needed, since an effective method is known to compute all the choices, Robinson's Unification in combination with the so-called Union-Find algorithm.
To briefly summarize the union-find algorithm, given the set of all types in a proof, it allows one to group them together into equivalence classes by means of a procedure and to pick a representative for each such class using a procedure. Emphasizing on the word procedure in the sense of side effect, we're clearly leaving the realm of logic to prepare an effective algorithm. The representative of a is determined such, that if both and are type variables the representative is arbitrarily one of them, while uniting a variable and a term, the term becomes the representative. Assuming an implementation of union-find at hand, one can formulate the unification of two monotypes as follows:
unify(ta,tb): ta = find(ta) tb = find(tb) if both ta,tb are terms of the form D p1..pn with identical D,n then unify(ta[i],tb[i]) for each corresponding ith parameter else if at least one of ta,tb is a type variable then union(ta,tb) else error 'types do not match'
Algorithm W
Algorithm W |
The presentation of Algorithm W as shown in the side box does not only deviate significantly from the original^{[4]} but is also a gross abuse of the notation of logical rules, since it includes side effects. It is legitimized here, for allowing a direct comparison with while expressing an efficient implementation at the same time. The rules now specify a procedure with parameters yielding in the conclusion where the execution of the premises proceeds from left to right. Alternatively to a procedure, it could be viewed as an attributation of the expression.
The procedure '' specializes the polytype by copying the term and replacing the bound type variables consistently by new monotype variables. '' produces a new monotype variable. Likely, has to copy the type introducing new variables for the qualification to avoid unwanted captures. Overall, the algorithm now proceeds by always making the most general choice leaving the specialization to the unification, which by itself produces the most general result. As noted above, the final result has to be generalized to in the end, to gain the most general type for a given expression.
Because the procedures used in the algorithm have near O(1) cost, the overall cost of the algorithm is close linear to the size of the expression for which a type is to be inferred. This is in strong contrast to many other attempts to derive type inference algorithms, which often came out to be NP-hard, if not undecidable w.r.t. termination. Thus the HM performs as good as the best fully informed type-checking algorithms can. Type-checking here means, that an algorithm does not have to find a proof, but only to validate a given one.
The efficiency is slightly lowered for two reasons. First, the binding of type variables in the context has to be maintained to allow computation of and an occurs check has to made to prevent the building of recursive types during . An example of such a case is , for which no type can be derived using HM. Because practically types are only small terms and do not build up expanding structures, one can treat them in complexity analysis as being smaller as some constant, retaining O(1) costs.
Original presentation of Algorithm W
In the original paper,^{[4]} the algorithm is presented more formally using a substitution style instead of side effects in the method above. In the later form, the side effect invisibly takes care of all places where a type variable is used. Explicitly using substitutions not only makes the algorithm hard to read, because the side effect occurs virtually everywhere, but also gives the false impression that the method might be costly. When implemented using purely functional means or for the purpose of proving the algorithm to be basically equivalent to the deduction system, full explicitness is of course needed and the original formulation a necessary refinement.
Further topics
Recursive definitions
A central property of the lambda calculus is, that recursive definitions are non-elemental, but can instead be expressed by a fixed point combinator. The original paper^{[4]} notes that recursion can realized by this combinator's type . A possible recursive definitions could thus be formulated as .
Alternatively an extension of the expression syntax and an extra typing rule is possible as:
where
basically merging and while including the recursively defined variables in monotype positions where they occur left to the but as polytypes right to it. This formulation perhaps best summarizes the essence of let-polymorphism.
Notes
- ↑ Polytypes are called "type schemes" in the original article.
- ↑ The parametric types were not present in the original paper on HM and are not needed to present the method. None of the inference rules below will take care or even note them. The same hold for the non-parametric "primitive types" in said paper. All the machinery for polymorphic type inference can be defined without them. They have been included here for sake of the examples but also because the nature of HM is all about parametric types. This comes from the function type , hard-wired in the inference rules, below, which already has two parameters and have been presented here as only a special case.
References
- ↑ R. Hindley, (1969) "The Principal Type-Scheme of an Object in Combinatory Logic", Transactions of the American Mathematical Society, Vol. 146, pp. 29–60 [1]
- ↑ Milner, (1978) "A Theory of Type Polymorphism in Programming". Journal of Computer and System Science (JCSS) 17, pp. 348–374[2]
- ↑ Luis Damas (1985): Type Assignment in Programming Languages. PhD thesis, University of Edinburgh (CST-33-85)
- ↑ ^{4.0} ^{4.1} ^{4.2} ^{4.3} ^{4.4} Damas, Milner (1982), "Principal type-schemes for functional programs". 9th Symposium on Principles of programming languages (POPL'82) pp. 207–212, ACM: [3]
- ↑ Clement, (1987). The Natural Dynamic Semantics of Mini-Standard ML. TAPSOFT'87, Vol 2. LNCS, Vol. 250, pp 67–81
- ↑ Jeff Vaughan. A proof of correctness for the Hindley–Milner type inference algorithm.[4]