
Hindley-Milner Type Inference (2012) - vyuh
http://steshaw.org/hm/
======
nestorD
Ocaml use Hindley-Milner type inference. I was taught programming with it and
ended up deeply spoiled.

I could not understand why most static language required writing so many types
that were easy to deduce and why people where saying that not having to write
types made dynamic language better.

I can count on the finger of my hands the number of times when I needed to
write explicit types in Ocaml (my single use case for explicit types in Ocaml
was data deserialization).

Plus, the compiler is fast.

~~~
wenc
Just curious, how would one handle something like this in Ocaml:

    
    
      x = function() { 
            if (rand() > 0.5) {
              return "abc"
            } else {
              return 2.01
            }
          }
    

So far, union types and boxing/unboxing are possible answers. Would type
inference work in this case? (well, it would for boxing/unboxing but union
types?)

~~~
jldugger
Type error. your lambda doesn't have a valid static type signature. You would
wrap that around a union type:

wtf = A of string | B of float

And then you'd return A("abc") or B(2.01) and your function would be () -> wtf

~~~
chrisseaton
Why's it a type error rather than inferring a sum type?

~~~
iaabtpbtpnn
You can get it to infer the sum type by giving tags to your values with
backticks, like `A 1.23 and `B "foo". This will be inferred as "`A of int | `B
of string" without you having to declare any type. Without those tags, the
system assumes you have made a mistake in returning values of separate types.
If it didn't do that, you would not get type errors in many/most (all?) of the
situations where you want the type system to help by indicating the
inconsistency to you.

~~~
chrisseaton
Giving tags seems like declaring a type though?

~~~
lmm
It's not though. If we think about Result as an example, `1`, `Success(1)` and
`Failure(1)` are different values, not just different types.

I think the point you're making is that Standard ML has certain restrictions
in its type system that are necessary to make type inference possible: there
are no union types, no subtypes (more recent work has found a way to make
subtyping compatible with full H-M), polymorphism has to be introduced
explicitly with `let`, and there are no higher-kinded types. And in some cases
you'd argue those restrictions might incur more code overhead compared to a
language that loosens those restrictions at the cost of less reliable type
inference (e.g. Scala).

That's sort-of-but-not-really true: in my experience union types and subtypes
are always bad ideas and there are better alternatives for any use case where
you would use them. You never actually want "String | Int", you want a type
with meaningful semantics (like `Result<String, Int>`).

------
recursivedoubts
Local type inference can give you 90% of the benefit at a fraction of the cost
of a global type inference system. Simply introducing local variable inference
probably captures 50+% of that and it's the simplest programming trick I ever
saw.

When I first came across it in the gosu code base ([https://gosu-
lang.github.io](https://gosu-lang.github.io)) I said:

"Wait. That's all you do? You just take the type from the right hand side and
then put it on the left hand side?"

"Yep."

"That's type inference?"

"Yep."

It called into question many aspects of my computer science education.

~~~
kccqzy
I agree. C++ has had some form of local type inference when it comes to
inferring template types, but in C++11 the auto keyword makes a tremendous
improvement.

Even in Haskell where global type inference is supported, people end up
putting type signatures on all top-level declarations because doing otherwise
makes code less readable and makes type error messages way too confusing.
Global type inference means a single type error in a single function can be
propagated by the compiler to arbitrarily faraway at the call site, or even
several layers deep in a call site. Solving such errors resulting from global
type inference simply isn't worth the time.

~~~
gnulinux
I've never written Haskell but I'm a Agda programmer (a Haskell dialect with
dependent types) and I _almost_ always put all the types and don't rely on
type inference. The obvious exception is if something quick like

    
    
      a = b
    

otherwise I'd always do

    
    
      a : Some -> Type -> MoreType
      a = f b
    

Readability is a reason why. Types are _extremely_ pedagogic tools. I believe
when I think about my code, I just think about types. I don't think about
computation, just the topology of types. Can I go to Int from X. If not why?
This is very different than when I write Python code. So seeing types in code
helps me a lot understand what's going on.

But the real reason is: errors. It's _so much_ more easier to understand
errors when types are explicit.

~~~
mijoharas
so, I'm curious, do you write the type of the function first?

When I used to write haskell, I'd very often write a function, iterate on it
until it did what I want, then infer the type of the function and insert that
into the program. Now, I was relatively new to haskell, so was probably less
comfortable writing types than reading them (so I assume it was nicer to have
the compiler do that for me, eyeball it to check it's what I meant to do, and
put it in).

I'm wondering if someone more familiar would think "alright, what type do I
want my function to be", fill that in, and then write the type out?

I definitely agree that a program is more readable with types, which is why I
always filled them in (I just did it afterwards!)

~~~
eru
I do both in Haskell. Sometimes I write the types first and sometimes I write
the function first.

Usually, the functions with more imperative flavour get their implementation
first, and the more functionally flavoured bits get their type first. But
that's not always the case.

Sometimes I even write the QuickCheck properties first.

------
ohazi
The great thing about languages that implement HM types is that they allow you
work as quickly or as carefully as you'd like without needing to change
languages.

If you've done something a million times before and are familiar with how it
works, you can leave off all the type hints, and it'll figure them out for you
and stay out of your way so that you can work more quickly. If you make a
mistake somewhere, the compiler will still tell you.

If you're doing something new, or don't quite understand something, you're
free to add type hints wherever you need them to gain clarity. You can even do
stuff like:

    
    
        let var: () = ...;
    

if you have no idea what type something is and just want to ask the compiler
for help.

Once you're done, you can tidy up if you think it makes your code more
readable. You can still leave top level hints to be kind to your coworkers or
future you.

I think the ability to dynamically shift between "cautious exploratory mode"
and "reckless I-know-what-I'm-doing mode" at the drop of a hat is why you hear
a lot of anecdotes about people using languages like OCaml or Rust as if they
were scripting languages.

~~~
CDSlice
To expand on this, if you use CLion as your Rust IDE it will actually show you
the types for all your variables automatically. For example, if you had a line
like this:

    
    
        let foo = vec![1, 2, 3];
    

CLion would then show the type in a faded font where the type would go if you
wrote it out manually like so:

    
    
        let foo: Vec<i32> = vec![1, 2, 3];
    

This is extremely useful when working with more complicated types such as
iterator chain because it lets you see exactly what type everything is without
having to manually write it all out and have the compiler check it.

~~~
riquito
That's available with VSCode and rust-analyzer too (but I suspect others that
piggyback on rust-analyzer got that ability). It can get noisy fast, but you
can toggle hints for when you need them

------
Kutta
This tutorial seems to miss the level-based generalization optimization, which
is crucial for production-strength HM inference. For, that you can look at:

[http://okmij.org/ftp/ML/generalization.html](http://okmij.org/ftp/ML/generalization.html)

~~~
shpongled
Level based generalization is a massive improvement in speed - I'm currently
writing a Standard ML compiler for fun, and I saw a 30-50% decrease in
elaboration type-checking duration when I switched to using levels. And it's
not difficult at all to implement.

------
FPreallyHurts
I had to implement this in Haskell while in university, we started with an
interpreter for a Scheme-like language, then added static typing and
inference.

Parser combinators, monads, monad transformers, curry-howard. Think for 15
minutes, write 5 lines of code, repeat. What I remember is that was code was
really elegant but brain hurt so much.

~~~
mcbuilder
Eventually you program enough FP, and then imperative starts to hurt your
brain. Programming is much like a muscle. As your program in one paradigm, you
brain starts to reason that way, then switch paradigms and you're suddenly
struck with a bunch of programming atrophy.

~~~
DaiPlusPlus
Imperative doesn’t hurt my brain, but because many operations in FP are
generally much more succinct and expressive than in an imperative style -
writing in imperative instead just makes me groan about all the manual
keyboard-typing I’ll have to do (e.g. Linq vs foreach).

I really wish I could do more FP, but the languages and libraries I use for my
day-job aren’t as-accommodating (mostly C#) - while C# has some FP features,
it’s really held-back by the CLR’s type-system - so until that fundamental
plumbing gets done we’ll never see features like type-classes, true immutable
types, algebraic-types, and so on. Without those features we’ll have to keep
on writing more code than is necessary.

<digress>Heck, it’s bad enough that IDictionary doesn’t implement
IReadOnlyDictionary - or that none of the IReadOnly* types make any guarantees
about immutability, which means having to review documentation or disassembly
in ILSpy. And the famed non-nullable-reference-types in C# 8.0 is actually all
just syntactic sugar for yet more attributes rather than true code-contracts,
a built-in Option type, or extending the CLR’s type-system to understand
nullability. Grumble.</digress>

~~~
Multicomp
> while C# has some FP features....we’ll never see features like type-classes,
> true immutable types, algebraic-types, and so on.

IIRC you just described some features of F#, excepting type classes. Or at
least F# gets you closer to the goal.

Maybe the ever elusive F* has that stuff?

------
atrudeau
Why is Damas dropped when referring to HM type inference? Not being critical,
just genuinely curious. I imagine there's a good reason.

------
dang
I put 2012 as an upper bound on the year above:
[https://web.archive.org/web/20120324053910/http://www.ian-
gr...](https://web.archive.org/web/20120324053910/http://www.ian-
grant.net/hm/). Perhaps it is earlier?

------
3001
Learnt this in my compiler class.It was the most brutal week of my life.

~~~
jldugger
IMO, the most brutal week was realizing that nothing you will ever use on the
job will use H-M type inference. Back to smashing rocks together.

~~~
Klathmon
Take it from someone who has to occasionally work on an over complicated type
system which uses HM inference, it ain't all that fun.

~~~
AnimalMuppet
Could you be more specific? What makes it painful?

(I mean, "overly complicated" is bad news no matter what the specifics of what
we're talking about, but how does it cause pain when it's specifically in the
types?)

~~~
Klathmon
For me personally it's just a lot of really heavy and very "meta" code that is
really hard for me to reason about. I also touch it very rarely, so there's
always a steep learning curve when I'm jumping back into it.

------
somewhereoutth
If my understanding is correct, type inference producing a set of possible
types - instead of a single type - for each term has proven to be undecidable?

This is a shame, because sum types (e.g. Shape := Circle U Square U Triangle)
are actually pretty useful, and languages such as Java ended up implementing
them via class or interface inheritance. Languages often also required type
coercion to deal with things like 0.2 + 5, otherwise + could have had a type
signature like: float U int -> float U int -> float U int.

~~~
marcosdumay
If my understanding is correct, yes, it's undecidable.

On practice nobody cares. It being undecidable just means you need an extra
check at your compiler to verify the types are converging, and a new error for
the problematic case.

------
siraben
One of my favorite resources that develop the HM algorithm from scratch is Ben
Lynn's[0], who treats it like a series of interview questions, because really
type inference algorithms match up two trees into constraints, which can
contain concrete types or type variables, then solves those constraints.

[0]
[https://crypto.stanford.edu/~blynn/lambda/hm.html](https://crypto.stanford.edu/~blynn/lambda/hm.html)

------
DanielBMarkham
I code both ways simultaneously.

I'm interested in what types I need to solve my business problem. The rest of
it, I don't care. The reason I care about the types I need for my business
problem is that I don't want my code doing something that it shouldn't: DDD-
style coding allows me to code in such a way that it's impossible for the
program to exist in a state that's invalid. That's a great time-saver. Other
than that, polymorphic coding FTW.

------
vyuh
_The algorithm, the type system, and some of the logical background are
explained in this tutorial, along with an implementation in standard ML._

[http://steshaw.org/hm/hindley-milner.pdf](http://steshaw.org/hm/hindley-
milner.pdf) I found this 30 page PDF document very helpful in understanding
the Algorithm.

------
hellofunk
One of the principal maintainers of the Racket language once gave a talk, I
think it was at the main Clojure conference, on a topic about Typed Racket,
where he strongly outlined the reasons why HM inference is a bad idea. It was
interesting to hear these differences in philosophies.

~~~
tommybu
Would you mind providing a link to this talk?

~~~
hellofunk
I've been searching for a few minutes and can't find it -- it was several
years ago, maybe at least 5 years, and at one of the Clojure conferences
(there used to be several big Clojure conferences each year back in the
language's golden age, but now there's 0 - 1 per year), and I can't remember
which conference.

~~~
andrekandre
was it this one?

[https://www.youtube.com/watch?v=RvHYr79RxrQ](https://www.youtube.com/watch?v=RvHYr79RxrQ)

~~~
hellofunk
That looks interesting as well, but it’s not the talk I was thinking of.

------
yawaramin
This may be my favourite explanation of the algorithm's logic:
[https://stackoverflow.com/a/42034379/20371](https://stackoverflow.com/a/42034379/20371)

------
fizixer
I'm starting to understand why HMTI, and OCaml/Haskell et. al., struggle from
such obscurity despite its fanboys swearing by it. The problem is that it's a
crap technology in both easy mode and hard mode.

\- easy mode: beginners just want to learn how to program as quickly as
possible, something like python is fantastic for that. And the side-benefit,
you can do amazing stuff with it, like bypass whole of symbolic AI of 80s and
90s (including HMTI, OCaml/Haskell) and go straight to deep learning, BERT,
GPT what not.

\- hard mode: mathematicians would love a helpful computing system that'll
make them more productive mathematicians. When they attempt to explore stuff
like Coq, and HoTT etc, the first thing that's thrown at their face is that
they have to give up law of excluded middle, non-constructive logic and what
not. I'm sorry but the tail does not wag the dog. If your theorem assistants
and checkers require the mathematicians to turn their world upside down, most
won't give a rat's ass about what you have to say about propositions-as-types
and programs-as-proofs.

There is a medium mode, or shall we say mediocre mode, whereby you fall in
love with type-theory and type-based languages, you want to live in your own
bubble and not be bothered by what's happening in the outside world. And
that's were the users of these systems live.

~~~
a1369209993
> that they have to give up law of excluded middle

They have to give up the law of the excluded middle because the law of the
excluded middle is _false_. Counterexample: "This statement is false.". You
can _pretend_ it's true by trying to outlaw self-reference, but Godel proved
that that doesn't actually work if your logic is useful enough to support
arithmetic.

~~~
kmill
I'm not sure what you're talking about, unless you're saying truth _is_
provability. (Classical logic would disagree.)

The Lean proof assistant, which has a pretty strong commitment to
constructability, has the law of the excluded middle just fine:
[https://github.com/leanprover/lean/blob/master/library/init/...](https://github.com/leanprover/lean/blob/master/library/init/classical.lean#L69)

The caveat is that it only exists in Prop, and it's a consequence of proof
irrelevance and functional extensionality. Types outside Prop don't have it
for the simple reason that there's no sensible way to construct an element of
Either A (A -> Void) in general (this is quasi-Haskell notation, where Void is
a type with no terms). I'm not sure there's any need to bring Godel into
this....

