Higher kinds go beyond the basic Hindley-Milner type system, but they can be handled in the same way.
Very roughly, HM parses the syntax tree of an expression, associates a free type variable to every subexpression, and generates a set of equational constraints over type-terms involving type variables according to the typing rules. The same can be done using higher kinds.
Then, the constraints are solved through unification. A typical step in the unification algorithm is (pseudo-haskell follows)
(F t1 ... tn := G s1 ... sk) =
| n/=k || F/=G -> fail
| otherwise -> { t1 := s1 , ... , tn := sn }
(Note that this is only a part of the unification algorithm.)
Above F
, G
are type constructor symbols, and not variables. In higher kinded unification, we need to account for F
,G
being variables as well.
We could try the following rule:
(f t1 ... tn := g s1 ... sk) =
| n/=k -> fail
| otherwise -> { f := g , t1 := s1 , ... , tn := sn }
But wait! The above is not correct, since e.g. f Int ~ Either Bool Int
must unify when f ~ Either Bool
. So, we need to also consider the case where n/=k
. In general, a simple rule set is
(f t := g s) =
{ f := g , t := s }
(F := G) = -- rule for atomic terms
| F /= G -> fail
| otherwise -> {}
(Again, this is only a part of the unification algorithm. Other cases must also be handled, as Andreas Rossberg points out below.)