Hey everyone. I've been implementing a small dependently typed language for fun and kept hitting a wall trying to decide: should I go with Hindley-Milner style global inference or bidirectional type checking? I ended up reading a lot of papers and thought this would be a good thread to summarize the key differences and get everyone's thoughts.
The Basics: What is Hindley-Milner?
A HindleyโMilner (HM) type system is a classical type system for the lambda calculus with parametric polymorphism, also known as DamasโMilner or DamasโHindleyโMilner. 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.
The key workhorse is Algorithm W. 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. HM is preferably used for functional programming languages. It was first implemented as part of the type system of ML. Since then, HM has been extended in various ways, most notably with type class constraints like those in Haskell.
Here's a sketch of the core unification-based inference (Algorithm J style):
Generics generally require a type system that supports unification. Unification is the process of assigning and solving type variables. HM sits in a sweet spot: the type system is quite expressive, and there are well known type inference algorithms that require absolutely no annotations from the programmer.
Let-Polymorphism and its Limits
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 example takes the form let id = ฮป x . x in ... (id 3) ... (id "text") ... which can be typed with a polymorphic type for 'id'.
I'll post more on bidirectional in the next reply. Curious what everyone thinks so far!