
What is Hindley-Milner and why is it cool? (2008) - jwdunne
http://www.codecommit.com/blog/scala/what-is-hindley-milner-and-why-is-it-cool
======
iitalics
Hindley-Milner is cool, but if you're planning on implementing a language with
type inference, I strongly suggest you take inspiration from bidirectional
type inference, notably Pierce-Turner[1] and subsequently Dunfield-
Krishnaswami[2]. These algorithms are capable of inferring much more complex
types and generally produce better error messages. Typically, there are very
few programs which can't be typed when you combine Dunfield-Krishnaswami type
inference and have the user place annotations on major function definitions.

[1] [http://www.cis.upenn.edu/~bcpierce/papers/lti-
toplas.pdf](http://www.cis.upenn.edu/~bcpierce/papers/lti-toplas.pdf)

[2] [https://arxiv.org/pdf/1306.6032.pdf](https://arxiv.org/pdf/1306.6032.pdf)

~~~
adrianratnapala
What does "bidirectional" mean in this context?

~~~
chewxy
It means check both ways

    
    
        Gamma |- e => t
    

Given Gamma (the environment) and e, you want to derive the type t (bog
standard type inference).

The other direction also holds:

    
    
        Gamma |- e <= t
    

Now we want to ensure that e conforms to type t.

Consider for example, a function:

    
    
        func foo(a) b
    

and an expression that we know the type of already:

    
    
        e <= a
    

Therefore we can say when we apply `foo` onto `e`, we'll yield something of
type `b`

In effect, you perform 2 passes: forwards inference, and then backwards
inference.

p/s: I'm not actually a PL person. This is from my casual reading, so my
understanding is highly probably very wrong

------
Animats
The trend seems to be for languages to have forward inference of types within
functions, while declaring them in function definitions. Assignments which
initialize a variable also implicitly declare its type. C++ started this with
"auto", but it's really because programmers had become used to dynamic
languages where assignment set type. Go and Rust both do this.

Inferring types of function parameters and results isn't as useful. Combined
with generics, it gets really complicated. It seems better to have those
declared explicitly. Programmers need to be able to read function signatures
when programming. They're an essential part of the documentation. Also, whole-
program type inference and separate compilation inherently conflict.

That's pretty much where things seem to be settling out. Even Python now has
optional type declarations for function signatures.

~~~
dbaupp
_> The trend seems to be for languages to have forward inference of types
within functions, while declaring them in function definitions. Assignments
which initialize a variable also implicitly declare its type. C++ started this
with "auto", but it's really because programmers had become used to dynamic
languages where assignment set type. Go and Rust both do this._

Note that this is quite different to, and simpler than, HM inference. It
doesn't require unification like the article describes and is something all
statically typed languages practically have to do anyway: even with full
annotations, validating `T x = f()` (or equivalent) requires type checking the
`f()` expression to see that it actually is a T. And thus, this style of
"inference" can be gained by simply not requiring the T type to be written and
just taking the result of type checking `f()` as gospel, and is what C++ and
Go do. (There's a bunch of subtlety/complexity for this in C++, but that's due
to C++, not this form of "inference".)

Proper HM (or similar) type inference will use information from how variables
are manipulated after their declaration to decide on the type, and so is more
than what's already done for the purposes of type checking. This is what Rust
does, breaking the supposed trend.

~~~
vilhelm_s
Although even with local type inference you usually want to do some
unification based inference also, at least to infer the type arguments for
generic functions. But it's still simpler than inferring the most general
types for functions themselves.

------
gergoerdi
One drawback of the Hindley-Milner type system is that its typecheckers are
necessarily non-compositional. The basic idea here is that this can result in
"non-symmetrical" error messages[1]: given something like

    
    
        MkPair :: a -> b -> Pair a b
        not :: Bool -> Bool
        succ :: Int -> Int
    
        foo x = MkPair (succ x) (not x)
    

the error message will either complain that `x` has type `Bool` but it should
be `Int`, or it will complain that `x` has type `Int` but it should be `Bool`
-- depending on what implementation you use! For example, in Haskell, Hugs 98
will emit the first error message, but GHC 7.10 will emit the second (see the
full example in my slides[2]). And in some sense, neither is right (and
certainly neither is as helpful for the programmer as it could be).

So next time you're implementing vanilla HM, maybe consider a compositional
type system[3] instead, which can give the following, much more informative
error message:

    
    
        Cannot unify 'Int' with 'Bool' when unifying 'x':
        Cannot unify 'Int' with 'Bool' in the following context:
        
               MkPair (succ x)        (not x)
           
               Bool -> Pair Int Bool  Bool
          x :: Int                    Bool
    

I have a simple implementation here[4], and am about to release a new version
which works on `haskell-src-ext`'s AST to support more of Haskell 98 syntax
(but still for vanilla HM only).

[1]
[https://gergo.erdi.hu/blog/2010-10-23-the_case_for_compositi...](https://gergo.erdi.hu/blog/2010-10-23-the_case_for_compositional_type_checking/)
[2]
[https://gergo.erdi.hu/talks/2016-06-compty/CompTy.pdf](https://gergo.erdi.hu/talks/2016-06-compty/CompTy.pdf)
[3]
[http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.25.8...](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.25.818)
[4] [https://github.com/gergoerdi/hm-
compo/blob/master/src/Langua...](https://github.com/gergoerdi/hm-
compo/blob/master/src/Language/HM/Compositional.hs)

------
krat0sprakhar
Hindley Milner is awesome and surpursingly not super hard to implement.
Shameless plug: Wrote a minimal implementation in OCaml back in college
[https://github.com/prakhar1989/type-
inference](https://github.com/prakhar1989/type-inference)

~~~
mbrock
I first became interested in programming language theory back in high school
when a guy in an IRC chat, who was a student at UIUC, asked me to do his
homework for him. The assignment was to implement Hindley-Milner type checking
with unification in OCaml. Thanks, lazy IRC guy!

~~~
jjtheblunt
Wow, good for you, ridiculous for the student's integrity; from UIUC also.

