Remove ads
Type system used in computer programming and mathematics From Wikipedia, the free encyclopedia
A Hindley–Milner (HM) type system is a classical type system for the lambda calculus with parametric polymorphism. It is also known as Damas–Milner or Damas–Hindley–Milner. It was 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 HM's more notable properties are its completeness and its ability to infer the most general type of a given program without programmer-supplied type annotations or other hints. Algorithm W is an efficient type inference method in practice and has been successfully applied on large code bases, although it has a high theoretical complexity.[note 1] 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 with type class constraints like those in Haskell.
As a type inference method, Hindley–Milner is able to deduce the types of variables, expressions and functions from programs written in an entirely untyped style. Being scope sensitive, it is not limited to deriving the types only from a small portion of source code, but rather from complete programs or modules. Being able to cope with parametric types, too, it is core to the type systems of many functional programming languages. It was first applied in this manner in the ML programming language.
The origin is the type inference algorithm for the simply typed lambda calculus that was devised by Haskell Curry and Robert Feys in 1958.[citation needed] In 1969, J. Roger Hindley extended this work and proved that their algorithm always inferred the most general type. In 1978, Robin Milner,[5] independently of Hindley's work, provided an equivalent algorithm, Algorithm W. In 1982, Luis Damas[4] finally proved that Milner's algorithm is complete and extended it to support systems with polymorphic references.
In the simply typed lambda calculus, types T are either atomic type constants or function types of form . Such types are monomorphic. Typical examples are the types used in arithmetic values:
3 : Number add 3 4 : Number add : Number -> Number -> Number
Contrary to this, the untyped lambda calculus is neutral to typing at all, and many of its functions can be meaningfully applied to all type of arguments. The trivial example is the identity function
which simply returns whatever value it is applied to. Less trivial examples include parametric types like lists.
While polymorphism in general means that operations accept values of more than one type, the polymorphism used here is parametric. One finds the notation of type schemes in the literature, too, emphasizing the parametric nature of the polymorphism. Additionally, constants may be typed with (quantified) type variables. E.g.:
cons : forall a . a -> List a -> List a nil : forall a . List a id : forall a . a -> a
Polymorphic types can become monomorphic by consistent substitution of their variables. Examples of monomorphic instances are:
id' : String -> String nil' : List Number
More generally, types are polymorphic when they contain type variables, while types without them are monomorphic.
Contrary to the type systems used for example in Pascal (1970) or C (1972), which only support monomorphic types, HM is designed with emphasis on parametric polymorphism. The successors of the languages mentioned, like C++ (1985), focused on different types of polymorphism, namely subtyping in connection with object-oriented programming and overloading. While subtyping is incompatible with HM, a variant of systematic overloading is available in the HM-based type system of Haskell.
When extending the type inference for the simply-typed lambda calculus towards polymorphism, one has to decide whether assigning a polymorphic type not only as type of an expression, but also as the type of a λ-bound variable is admissible. This would allow the generic identity type to be assigned to the variable 'id' in:
(λ id . ... (id 3) ... (id "text") ... ) (λ x . x)
Allowing this gives rise to the polymorphic lambda calculus; however, unfortunately, type inference in this system is not decidable.[6] Instead, HM distinguishes variables that are immediately bound to an expression from more general λ-bound variables, calling the former let-bound variables, and allows polymorphic types to be assigned only to these. This leads to let-polymorphism where the above example takes the form
let id = λ x . x in ... (id 3) ... (id "text") ...
which can be typed with a polymorphic type for 'id'. As indicated, the expression syntax is extended to make the let-bound variables explicit, and by restricting the type system to allow only let-bound variable to have polymorphic types, while the parameters in lambda-abstractions must get a monomorphic type, type inference becomes decidable.
The remainder of this article proceeds as follows:
The same description of the deduction system is used throughout, even for the two algorithms, to make the various forms in which the HM method is presented directly comparable.
The type system can be formally described by syntax rules that fix a language for the expressions, types, etc. The presentation here of such a syntax is not too formal, in that it is written down not to study the surface grammar, but rather the depth grammar, and leaves some syntactical details open. This form of presentation is usual. Building on this, typing rules are used to define how expressions and types are related. As before, the form used is a bit liberal.
Expressions |
Types |
Context and Typing |
Free Type Variables |
The expressions to be typed are exactly those of the lambda calculus extended with a let-expression as shown in the adjacent table. Parentheses can be used to disambiguate an expression. The application is left-binding and binds stronger than abstraction or the let-in construct.
Types are syntactically split into two groups, monotypes and polytypes.[note 2]
Monotypes always designate a particular type. Monotypes are syntactically represented as terms.
Examples of monotypes include type constants like or , and parametric types like . The latter types are examples of applications of type functions, for example, from the set , where the superscript indicates the number of type parameters. The complete set of type functions is arbitrary in HM,[note 3] except that it must contain at least , the type of functions. It is often written in infix notation for convenience. For example, a function mapping integers to strings has type . Again, parentheses can be used to disambiguate a type expression. The application binds stronger than the infix arrow, which is right-binding.
Type variables are admitted as monotypes. Monotypes are not to be confused with monomorphic types, which exclude variables and allow only ground terms.
Two monotypes are equal if they have identical terms.
Polytypes (or type schemes) are types containing variables bound by zero or more for-all quantifiers, e.g. .
A function with polytype 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. A function which returns the cardinality of a set would be a value of this type.
Quantifiers can only appear top level. For instance, a type is excluded by the syntax of types. Also monotypes are included in the polytypes, thus a type has the general form , where and is a monotype.
Equality of polytypes is up to reordering the quantification and renaming the quantified variables (-conversion). Further, quantified variables not occurring in the monotype can be dropped.
To meaningfully bring together the still disjoint parts (syntax expressions and types) a third part is needed: context. Syntactically, a context is a list of pairs , called assignments, assumptions or bindings, each pair stating that value variable has type All three parts combined give a typing judgment of the form , stating that under assumptions , the expression has type .
In a type , the symbol is the quantifier binding the type variables in the monotype . The variables are called quantified and any occurrence of a quantified type variable in is called bound and all unbound type variables in are called free. Additionally to the quantification in polytypes, type variables can also be bound by occurring in the context, but with the inverse effect on the right hand side of the . Such variables then behave like type constants there. Finally, a type variable may legally occur unbound in a typing, in which case they are implicitly all-quantified.
The presence of both bound and unbound type variables is a bit uncommon in programming languages. Often, all type variables are implicitly treated all-quantified. For instance, one does not have clauses with free variables in Prolog. Likewise in Haskell, [note 4] where all type variables implicitly occur quantified, i.e. a Haskell type a -> a
means here. Related and also very uncommon is the binding effect of the right hand side of the assignments.
Typically, the mixture of both bound and unbound type variables originate from the use of free variables in an expression. The constant function K = provides an example. It has the monotype . One can force polymorphism by . Herein, has the type . The free monotype variable originates from the type of the variable bound in the surrounding scope. has the type . One could imagine the free type variable in the type of be bound by the in the type of . But such a scoping cannot be expressed in HM. Rather, the binding is realized by the context.
Polymorphism means that one and the same expression can have (perhaps infinitely) many types. But in this type system, these types are not completely unrelated, but rather orchestrated by the parametric polymorphism.
As an example, the identity can have as its type as well as or and many others, but not . The most general type for this function is , while the others are more specific and can be derived from the general one by consistently replacing another type for the type parameter, i.e. the quantified variable . The counter-example fails because the replacement is not consistent.
The consistent replacement can be made formal by applying a substitution to the term of a type , written . As the example suggests, substitution is not only strongly related to an order, that expresses that a type is more or less special, but also with the all-quantification which allows the substitution to be applied.
Specialization Rule |
Formally, in HM, a type is more general than , formally , if some quantified variable in is consistently substituted such that one gains as shown in the side bar. This order is part of the type definition of the type system.
In our previous example, applying the substitution would result in .
While substituting a monomorphic (ground) type for a quantified variable is straight forward, substituting a polytype has some pitfalls caused by the presence of free variables. Most particularly, unbound variables must not be replaced. They are treated as constants here. Additionally, quantifications can only occur top-level. Substituting a parametric type, one has to lift its quantifiers. The table on the right makes the rule precise.
Alternatively, consider an equivalent notation for the polytypes without quantifiers in which quantified variables are represented by a different set of symbols. In such a notation, the specialization reduces to plain consistent replacement of such variables.
The relation is a partial order and is its smallest element.
While specialization of a type scheme is one use of the order, it plays a crucial second role in the type system. Type inference with polymorphism faces the challenge of summarizing all possible types an expression may have. The order guarantees that such a summary exists as the most general type of the expression.
The type order defined above can be extended to typings because the implied all-quantification of typings enables consistent replacement:
Contrary to the specialisation rule, this is not part of the definition, but like the implicit all-quantification rather a consequence of the type rules defined next. Free type variables in a typing serve as placeholders for possible refinement. The binding effect of the environment to free type variables on the right hand side of that prohibits their substitution in the specialisation rule is again that a replacement has to be consistent and would need to include the whole typing.
This article will discuss four different rule sets:
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. The examples below show 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.
Declarative Rule System |
The side box shows the deduction rules of the HM type system. One can roughly divide the rules into two groups:
The first four rules (variable or function access), (application, i.e. function call with one parameter), (abstraction, i.e. function declaration) and (variable declaration) are centered around the syntax, presenting one rule for each of the expression forms. Their meaning is 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 allows generalization, i.e. to quantify monotype variables not bound in the context.
The following two examples exercise the rule system in action. Since both the expression and the type are given, they are a type-checking use of the rules.
Example: A proof for where , could be written
Example: To demonstrate generalization, is shown below:
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 . Remember that and denote poly- and monotypes respectively.
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 in the context prevents the use of the generalisation rule for any free variable in the assignment, this regulation forces the type of parameter in a -expression to remain monomorphic, while in a let-expression, the variable could be introduced polymorphic, making specializations possible.
As a consequence of this regulation, cannot be typed, since the parameter is in a monomorphic position, while has type , because has been introduced in a let-expression and is treated polymorphic therefore.
The generalisation rule is also worth a closer look. Here, the all-quantification implicit in the premise is simply moved to the right hand side of in the conclusion, bound by an explicit universal quantifier. This is possible, since does not occur free in the context. Again, while this makes the generalization rule plausible, it is not really a consequence. On the contrary, the generalization rule is part of the definition of HM's type system and the implicit all-quantification a consequence.
Now that the deduction system of HM is at hand, one could present an algorithm and validate it with respect to 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.
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 proofs 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 not be needed. This is in fact possible and leads to a variant of the rules system with no such rules.
Syntactical Rule System |
Generalization |
A contemporary treatment of HM uses a purely syntax-directed rule system due to Clement[7] 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 quantifies 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 decomposes 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 [8] though, namely
implying, one can derive the principal type for an expression in allowing us to generalize the proof in the end.
Comparing and , now only monotypes appear in the judgments of all rules. Additionally, the shape of any possible proof with the deduction system is now identical to the shape of the expression (both seen as trees). Thus the expression fully determines the shape of the proof. In the shape would likely be determined with respect to all rules except and , which allow building arbitrarily long branches (chains) between the other nodes.
Now that the shape of the proof is known, one is already close to formulating a type inference algorithm. Because any proof for a given expression must have the same shape, one can assume the monotypes in the proof's judgements to be undetermined and consider how to determine them.
Here, the substitution (specialisation) order comes into play. Although at the first glance one cannot determine the types locally, the hope is that it is possible to refine them with the help of the order while traversing the proof tree, additionally assuming, because the resulting algorithm is to become an inference method, that the type in any premise will be determined as the best possible. And in fact, one can, as looking at the rules of suggests:
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 union procedure and to pick a representative for each such class using a find procedure. Emphasizing the word procedure in the sense of side effect, we're clearly leaving the realm of logic in order to prepare an effective algorithm. The representative of a is determined such that, if both a and b are type variables then the representative is arbitrarily one of them, but 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'
Now having a sketch of an inference algorithm at hand, a more formal presentation is given in the next section. It is described in Milner[2] P. 370 ff. as algorithm J.
Algorithm J |
The presentation of Algorithm J is a misuse of the notation of logical rules, since it includes side effects but allows 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.
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 quantification 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 nearly O(1) cost, the overall cost of the algorithm is close to linear in 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 with respect to termination. Thus the HM performs as well 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.
Efficiency is slightly reduced because the binding of type variables in the context has to be maintained to allow computation of and enable an occurs check to prevent the building of recursive types during . An example of such a case is , for which no type can be derived using HM. Practically, types are only small terms and do not build up expanding structures. Thus, in complexity analysis, one can treat comparing them as a constant, retaining O(1) costs.
In the previous section, while sketching the algorithm its proof was hinted at with metalogical argumentation. While this leads to an efficient algorithm J, it is not clear whether the algorithm properly reflects the deduction systems D or S which serve as a semantic base line.
The most critical point in the above argumentation is the refinement of monotype variables bound by the context. For instance, the algorithm boldly changes the context while inferring e.g. , because the monotype variable added to the context for the parameter later needs to be refined to when handling application. The problem is that the deduction rules do not allow such a refinement. Arguing that the refined type could have been added earlier instead of the monotype variable is an expedient at best.
The key to reaching a formally satisfying argument is to properly include the context within the refinement. Formally, typing is compatible with substitution of free type variables.
To refine the free variables thus means to refine the whole typing.
Algorithm W |
From there, a proof of algorithm J leads to algorithm W, which only makes the side effects imposed by the procedure explicit by expressing its serial composition by means of the substitutions . The presentation of algorithm W in the sidebar still makes use of side effects in the operations set in italic, but these are now limited to generating fresh symbols. The form of judgement is , denoting a function with a context and expression as parameter producing a monotype together with a substitution. is a side-effect free version of producing a substitution which is the most general unifier.
While algorithm W is normally considered to be the HM algorithm and is often directly presented after the rule system in literature, its purpose is described by Milner[2] on P. 369 as follows:
While he considered W more complicated and less efficient, he presented it in his publication before J. It has its merits when side effects are unavailable or unwanted. W is also needed to prove completeness, which is factored by him into the soundness proof.
Before formulating the proof obligations, a deviation between the rules systems D and S and the algorithms presented needs to be emphasized.
While the development above sort of misused the monotypes as "open" proof variables, the possibility that proper monotype variables might be harmed was sidestepped by introducing fresh variables and hoping for the best. But there's a catch: One of the promises made was that these fresh variables would be "kept in mind" as such. This promise is not fulfilled by the algorithm.
Having a context , the expression cannot be typed in either or , but the algorithms come up with the type , where W additionally delivers the substitution , meaning that the algorithm fails to detect all type errors. This omission can easily be fixed by more carefully distinguishing proof variables and monotype variables.
The authors were well aware of the problem but decided not to fix it. One might assume a pragmatic reason behind this. While more properly implementing the type inference would have enabled the algorithm to deal with abstract monotypes, they were not needed for the intended application where none of the items in a preexisting context have free variables. In this light, the unneeded complication was dropped in favor of a simpler algorithm. The remaining downside is that the proof of the algorithm with respect to the rule system is less general and can only be made for contexts with as a side condition.
The side condition in the completeness obligation addresses how the deduction may give many types, while the algorithm always produces one. At the same time, the side condition demands that the type inferred is actually the most general.
To properly prove the obligations one needs to strengthen them first to allow activating the substitution lemma threading the substitution through and . From there, the proofs are by induction over the expression.
Another proof obligation is the substitution lemma itself, i.e. the substitution of the typing, which finally establishes the all-quantification. The later cannot formally be proven, since no such syntax is at hand.
To make programming practical recursive functions are needed. A central property of the lambda calculus is that recursive definitions are not directly available, but can instead be expressed with a fixed point combinator. But unfortunately, the fixpoint combinator cannot be formulated in a typed version of the lambda calculus without having a disastrous effect on the system as outlined below.
The original paper[4] shows recursion can be realized by a combinator . A possible recursive definition could thus be formulated as .
Alternatively an extension of the expression syntax and an extra typing rule is possible:
where
basically merging and while including the recursively defined variables in monotype positions where they occur to the left of the but as polytypes to the right of it.
While the above is straightforward it does come at a price.
Type theory connects lambda calculus with computation and logic. The easy modification above has effects on both:
Overloading means that different functions can be defined and used with the same name. Most programming languages at least provide overloading with the built-in arithmetic operations (+, <, etc.), to allow the programmer to write arithmetic expressions in the same form, even for different numerical types like int
or real
. Because a mixture of these different types within the same expression also demands for implicit conversion, overloading especially for these operations is often built into the programming language itself. In some languages, this feature is generalized and made available to the user, e.g. in C++.
While ad hoc overloading has been avoided in functional programming for the computation costs both in type checking and inference[citation needed], a means to systematise overloading has been introduced that resembles both in form and naming to object oriented programming, but works one level upwards. "Instances" in this systematic are not objects (i.e. on value level), but rather types. The quicksort example mentioned in the introduction uses the overloading in the orders, having the following type annotation in Haskell:
quickSort :: Ord a => [a] -> [a]
Herein, the type a
is not only polymorphic, but also restricted to be an instance of some type class Ord
, that provides the order predicates <
and >=
used in the functions body. The proper implementations of these predicates are then passed to quicksorts as additional parameters, as soon as quicksort is used on more concrete types providing a single implementation of the overloaded function quickSort.
Because the "classes" only allow a single type as their argument, the resulting type system can still provide inference. Additionally, the type classes can then be equipped with some kind of overloading order allowing one to arrange the classes as a lattice.
Parametric polymorphism implies that types themselves are passed as parameters as if they were proper values. Passed as arguments to a proper functions, but also into "type functions" as in the "parametric" type constants, leads to the question how to more properly type types themselves. Higher-order types are used to create an even more expressive type system.
Unfortunately, unification is no longer decidable in the presence of meta types, rendering type inference impossible in this extend of generality. Additionally, assuming a type of all types that includes itself as type leads into a paradox, as in the set of all sets, so one must proceed in steps of levels of abstraction. Research in second order lambda calculus, one step upwards, showed that type inference is undecidable in this generality.
Haskell introduces one higher level named kind. In standard Haskell, kinds are inferred and used for little more than to describe the arity of type constructors. e.g. a list type constructor is thought of as mapping a type (the type of its elements) to another type (the type of the list containing said elements); notationally this is expressed as . Language extensions are available which extend kinds to emulate features of a dependent type system.[9]
Attempts to combine subtyping and type inference have caused quite some frustration. It is straightforward to accumulate and propagate subtyping constraints (as opposed to type equality constraints), making the resulting constraints part of the inferred typing schemes, for example , where is a constraint on the type variable . However, because type variables are no longer unified eagerly in this approach, it tends to generate large and unwieldy typing schemes containing many useless type variables and constraints, making them hard to read and understand. Therefore, considerable effort was put into simplifying such typing schemes and their constraints, using techniques similar to those of nondeterministic finite automaton (NFA) simplification (useful in the presence of inferred recursive types).[10] More recently, Dolan and Mycroft[11] formalized the relationship between typing scheme simplification and NFA simplification and showed that an algebraic take on the formalization of subtyping allowed generating compact principal typing schemes for an ML-like language (called MLsub). Notably, their proposed typing scheme used a restricted form of union and intersection types instead of explicit constraints. Parreaux later claimed[12] that this algebraic formulation was equivalent to a relatively simple algorithm resembling Algorithm W, and that the use of union and intersection types was not essential.
On the other hand, type inference has proven more difficult in the context of object-oriented programming languages, because object methods tend to require first-class polymorphism in the style of System F (where type inference is undecidable) and because of features like F-bounded polymorphism. Consequently, type systems with subtyping enabling object-oriented programming, such as Cardelli's system ,[13] do not support HM-style type inference.
Row polymorphism can be used as an alternative to subtyping for supporting language features like structural records.[14] While this style of polymorphism is less flexible than subtyping in some ways, notably requiring more polymorphism than strictly necessary to cope with the lack of directionality in type constraints, it has the advantage that it can be integrated with the standard HM algorithms quite easily.
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.