4

AFAIK, unification used in the Hindley-Milner type system can be generalized to unify higher-kinded types by allowing type vars in constructor position and by relaxing the arity constraint in such cases:

f a ~ T a1 b1
f ~ T a1 -- generatifity + partial application
a ~ b1   -- injectivity

I guess kinds are also involved, but I don't know how.

With my little experience I'd say this is enough for a sound unification of higher-kinded types. The main differences to higher-order unification are probably

  • generalized HM is decidable, HOU is not in general
  • generalized HM is more restricted, i.e. rejects types that are legal in HOU

While I somehow answered my question what higher-order unification gives us, I don't have a clue what (simplified) rules are involved when higher-order unification is applied. How does the algorithm differ from generalized HM?

  • 1
    I think that if you write types with explicit application as in `app(f, a) ~ app(app(T, a1), b1)` which follows the actual AST in the compiler/type checker, then you can use standard unification. – chi Feb 01 '21 at 14:00
  • Well, if you agree that HOU is undecidable, then the question "How does the [HOU] algorithm differ from generalized HM?" doesn't make much sense, since there cannot *be* an HOU algorithm. That's the only sentence with a question mark here, which makes this post really hard to respond to sensibly. – Daniel Wagner Feb 01 '21 at 15:36
  • @DanielWagner Fair point. I was under the impression that HOU is only undecidable under certain circumstances and that you can restrict the type system accordingly, kind of like System Fω? Sorry for being so handwavy. I lack the necessary knowlegde in logic/type theory. –  Feb 01 '21 at 16:13
  • So... what is your question, then? – Daniel Wagner Feb 01 '21 at 16:15
  • 1
    It’s also worth noting that while injectivity & generativity are necessary, a big part of keeping things 1st-order is just disallowing partially applied type synonyms, since they’d be equivalent to type-level lambdas. This is closely related to impredicativity: you can’t put a `forall` type as the argument of a constructor (besides `(->)` with `RankNTypes`), unless you wrap it in a `newtype`—which is usually explained as “hiding” the impredicativity, but can also be thought of as telling the compiler where to generalise & instantiate the `forall` ≈ abstract & apply a type-level lambda. – Jon Purdy Feb 02 '21 at 00:23

1 Answers1

4

This isn't really my wheelhouse, but I can maybe offer a very general answer.

The fundamental difference is that generalized HM treats unification as a purely syntactic matching process aimed at producing a unique match, while an HOU algorithm involves a semantic consideration of the types/kinds (as terms/types in a typed lambda calculus) and a systematic search through a tree of possible unifications, including consideration of alternative unifications at interior nodes.

(The reason for the limitations of the HM approach is that for first-order types, purely syntactic matching is basically equivalent to a semantic consideration of the types and a systematic search through possibly unifications.)

Anyway, take a trivial higher-order unification:

Either String Int ~ f String

Your proposed generalized HM algorithm fails on this unification for the absurd reason that Either's arguments are in the wrong order, a purely syntactic detail that has nothing to do with the types' semantic unifiability. You could further generalize your algorithm to handle this particular case syntactically, but there would invariably be some other trivial unification that wouldn't match the syntactic pattern. You'd also end up with a bizarre "discontinuity" at the unification:

Either String String ~ f String

You'd be able to get your algorithm to type-check a program with unifications:

Either Int String ~ f Int
Either String String ~ f String
 ==> f x = Either x String

or:

Either String Int ~ f Int
Either String String ~ f String
 ==> f x = Either String x

but presumably not both.

In contrast, any self-respecting HOU algorithm would have no trouble type-checking these programs.

HOU algorithms based on Huet's algorithm do so by constructing a "matching tree". Each node in the tree is labelled with a "disagreement set" (basically, a set of unresolved unifications), with branches labelled with alternative substitutions. Terminal nodes indicate "success" or "failure" of the unification.

Example 3.2 presented in Huet's paper is the unification:

f x A ~ B

Any generalized HM would immediately give up, as the type B, being of kind *, can't syntactically unify with a type expression involving f :: * -> * -> *.

For a Huet-like algorithm, the matching tree is constructed with this singleton disagreement set at the root node with three possible kind-correct substitutions for f on its branches:

f :: * -> * -> *
f u v = u
f u v = v
f u v = B

giving the tree:

                        f x A ~ B
                             |
        --------------------------------------------
        | (f u v = u)        | (f u v = v)         | (f u v = B)
        |                    |                     |
      x ~ B              Failure                 Success
        |
        | (x = B)
        |
      Success

If you give it a moment's consideration, you'll see that the power of a generalized HM type checker versus an HOU checker isn't even remotely comparable. You'll also see that an HOU type checker in practice can be a power that a programmer might find hard to control. It's maybe a little hard to reason about a type checker that can deduce either f x = Either x String or f x = Either String x.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • 1
    After a first skim of your answer I can already say that it contains just the right amount of information and requires just the right amount of prior knowledge to be comprehensable to me. Thank you! –  Feb 01 '21 at 20:24
  • 4
    I note here that Huet's algorithm is not the one commonly used in language implementations, that's Miller's pattern unification instead. Pattern unification does not inspect types of sides (in its most basic version), merely assumes that sides have the same type, nor does it search for possible solutions. Also, since `Either String Int ~ f String` does not have a unique solution, pattern unification necessarily fails on it. – András Kovács Feb 01 '21 at 20:33
  • 2
    And to make it even harder, there are still more solutions for `f`, like `f u v = if v == A then B else u`; indeed infinitely many in any "rich enough" type language. – Daniel Wagner Feb 01 '21 at 23:22