News for hackers. Stuff that matters.
Re:Hindley-Milner Type Inference (2012) by chrisseaton on Friday July 10, 2020 @03:41PM (#23795507)
> I can count on the finger of my hands the number of times when I needed to write explicit types in Ocaml

Do you genuinely rely on whole-program type inference? Writing fewer than ten explicit types in your whole career writing OCaml really surprises me. For example even the OCaml standard library is not written this way.

I find in practice most people explicitly type their top-level functions as if you don't this it can cause type errors at the point of use to 'poison' the function types and cause very confusing errors far away in the program. Do you not see this when you program without any explicit types?

We can see this in other projects for example Crystal which had to back-track on their goal of whole program inference, because it just doesn't really seem tractable in practice.

Re:Hindley-Milner Type Inference (2012) by GregarianChild on Friday July 10, 2020 @04:05PM (#23795792)
Why not let the computer do the work? My programming flow in Ocaml has always been:

(1) Hack until I've got something stable. Note: at the start of coding I typically don't quite know what the right abstractions are, so a lot of code (and associated types) are in constant flux.

(2) Once program is stable, wrap it in a module, let the compiler work out all types and put them in as module interface.

That gives me the best of all possible worlds: type annotations at abstraction boundaries but you don't have to type them out. Writing out explicit types inhibits fast change. I wish the languages I'm using at the moment (Scala, Rust, C++) allowed a flow like that. Alas, full type inference stops being possible with more expressive typing systems.

Re:Hindley-Milner Type Inference (2012) by MaxBarraclough on Friday July 10, 2020 @06:53PM (#23797531)
> Why not let the computer do the work?

Because a program's types can serve as part of its documentation.

Re:Hindley-Milner Type Inference (2012) by hombre_fatal on Friday July 10, 2020 @08:40PM (#23798309)
The other benefit of top-level/function annotations is that they ensure that your mental model of the code is consistent with what the code actually does.

So many times I will accidentally return 42 or Result<Result<42>> when I mean to return Result<42>. With annotations, my error is instantly obvious. Otherwise I accidentally have a function that returns Result<Result<number>> and it may lead to a very weird error message when I use `foo().map(_ + 100)` somewhere else.

That's letting the computer doing more work, not less of it. Relying too much on inference can bow back into the human doing more of the work to figure out what the types actually are in a given spot.

Re:Hindley-Milner Type Inference (2012) by lmm on Friday July 10, 2020 @11:01PM (#23799096)
Sometimes an IDE hover can be the best of both worlds - the information is there when you want it (and guaranteed to be accurate since it's using the language compiler's own type inference), but not taking up space when you don't want it.
Re:Hindley-Milner Type Inference (2012) by eru on Saturday July 11, 2020 @12:26AM (#23799496)
That helps, but it's orthogonal to adding type annotations.

The computer can only tell you what types your code _has_ (from inference), but not what type it _should_ have based on your intentions. Type inference can only complain when your code contradicts with itself (on a type level), but can't tell you when it contradicts with your intentions.

If you don't write out your intentions, you'd have to hover over everything to see if the inferred types differ from your expectations.

IDE hover types and collapsible type annotations (and alternatively also collapsible implementation) are still useful as a way to declutter your UI when reading code.

Re:Hindley-Milner Type Inference (2012) by mathgladiator on Friday July 10, 2020 @07:29PM (#23797828)
This is under-appreciated, and as I have aged I have discovered that I prefer the verbosity over brevity. Namely, when mentoring more junior engineers who can barely write code at all.
Re:Hindley-Milner Type Inference (2012) by mrmr1993 on Saturday July 11, 2020 @12:18AM (#23799440)
It doesn't always make sense to plan the types ahead of time; letting the compiler do the work while you code and then creating a nice interface after is a reasonable way to work.
Re:Hindley-Milner Type Inference (2012) by chrisseaton on Friday July 10, 2020 @04:11PM (#23795861)
> let the compiler work out all types and put them in as module interface ... type annotations at abstraction boundaries

Right - that's not global type inference. It's intra-module type-inference.

Re:Hindley-Milner Type Inference (2012) by GregarianChild on Friday July 10, 2020 @04:18PM (#23795958)
I don't understand this objection. OCaml does full program inference. I just prefer to wrap things into modules from time to time. That's a pragmatic choice I make, not a restriction on type-inference.

Aside, when I decide to change where I put module boundaries, full program inference helps too: I just throw away the old module definitions, draw different boundaries and re-run inference.

Re:Hindley-Milner Type Inference (2012) by chrisseaton on Friday July 10, 2020 @04:21PM (#23795988)
I'm not objecting to anything. This is how I thought most people used inference in OCaml.
Re:Hindley-Milner Type Inference (2012) by amw-zero on Friday July 10, 2020 @07:09PM (#23797671)
You do need to write the types for records though.
Re:Hindley-Milner Type Inference (2012) by NOGDP on Friday July 10, 2020 @11:23PM (#23799199)
Is there anything specific that you find Rust is missing in this regard?
Re:Hindley-Milner Type Inference (2012) by eru on Saturday July 11, 2020 @12:27AM (#23799500)
I think Rust deliberately limits type inference to information available in a single function?
Re:Hindley-Milner Type Inference (2012) by steveklabnik on Saturday July 11, 2020 @12:15PM (#23803227)
Yes.
Re:Hindley-Milner Type Inference (2012) by herbstein on Friday July 10, 2020 @04:10PM (#23795855)
> Do you genuinely rely on whole-program type inference?

I think their point is that they there were vanishingly few times where they were required to write explicit type annotations. As they say, mostly when dealing with data serialization. That doesn't say whether they wrote more, however.

Re:Hindley-Milner Type Inference (2012) by a1369209993 on Friday July 10, 2020 @10:30PM (#23798960)
Except that would then not count the many times where they 'weren't' required to write explicit type annotations solely because they had already written those annotations. Which is not necessarily a unreasonable distinction to make[0], but I can say the same even about C: there have been less than ten times that I got a compiler error because I declared a function or variable without specifying a type for it.

0: There are plenty of places where not supplying a type annotation is more trouble than just writing it. Eg (Haskell since I'm not familiar with OCaml offhand):

  data Foo = FooBar Int String
           | FooBaz [Int]
I'm not even sure how to avoid writing explicit type annotations for Int, String, and [Int] there; maybe OCaml makes it easy, but I doubt I'd bother literally all but less than ten times.
Re:Hindley-Milner Type Inference (2012) by yawaramin on Saturday July 11, 2020 @01:20AM (#23799733)
> I'm not even sure how to avoid writing explicit type annotations for Int, String, and [Int] there; maybe OCaml makes it easy

It does make it easy. OCaml has a full matrix of types:

- Nominal product types (records)

- Nominal sum types (variants)

- Structural product types (objects)

- Structural sum types (polymorphic variants)

You could actually always use structural types and never have to actually define types. But OCaml people actually really like the nominal types because their typing rules are stricter and easier for people to understand. But the structural types can come in really handy, e.g. they give you a very convenient way to encode data between libraries that don't need to know about each other. E.g. there is an effort to add JSON-structured output to the OCaml compiler itself. This is using a polymorphic variant type to represent the JSON data type, to avoid having a dependency on any particular JSON library, but still being compatible with popular OCaml JSON libraries ( https://ocaml-community.github.io/yojson/yojson/Yojson/index... ).

Re:Hindley-Milner Type Inference (2012) by eru on Saturday July 11, 2020 @12:29AM (#23799510)
That's a type definition. You can't get away without writing out that definition.

If you wanted to, you could use

    Either (Int, String) [Int]
in your code and rely on type inference to do all the work.
Re:Hindley-Milner Type Inference (2012) by a1369209993 on Saturday July 11, 2020 @01:11AM (#23799695)
> You can't get away without writing out that definition.

Not in Haskell as far as I know, but (assuming MonomorphismRestriction) there's no technical reason why

  data Qwe = Q a
  qwe_eps :: Qwe
  qwe_eps = Q ""
couldn't infer a ~ String in the type definition for Qwe. It's just annoying and pointless, because you never actually care about having to write a explicit type annotation there. (Which was my point.) But it's still a explicit type annotation that you're required to write. (Which was (kind of) chrisseaton's point.)
Re:Hindley-Milner Type Inference (2012) by NOGDP on Friday July 10, 2020 @11:29PM (#23799218)
I guess the idea is:

data Foo a b c = FooBar a b | FooBaz c

or

data Foo a b = FooBar a b | FooBaz [a]

But I agree that writing explicit type annotations is overall better practice.

Re:Hindley-Milner Type Inference (2012) by a1369209993 on Friday July 10, 2020 @11:50PM (#23799305)
That declares 'Foo' with kind * -> * -> * -> * , ie a generic type. Whereas I was declaring with kind * , a specific type that might have infered components. Compare:

  quux :: a -> a
  quux x = x
  # quux takes any type genericly
  
  frob y = y () ++ [()]
  # frob takes some specific type, but we don't need to specify which (spoiler alert: it's () -> [()])
Edit: actually the type is generic (not polymorphic) as well.
Re:Hindley-Milner Type Inference (2012) by NOGDP on Sunday July 12, 2020 @12:07AM (#23808718)
Why does the kind matter? Both of your examples use type inference and polymorphism - the type 'a' is inferred when quux is used, the type for the second example is inferred from the polymorphic types of (++) and `$`, and both allow to code more freely while in flux.
Re:Hindley-Milner Type Inference (2012) by eatonphil on Friday July 10, 2020 @03:58PM (#23795703)
How much simpler would OCaml or Standard ML be if there were no type inference? I'm perfectly happy to never use type inference (maybe only for local variables).

It feels like people get distracted by H-M inference and forget that there's a pretty nice high level language in SML or OCaml anyway (although I'd rather have modular implicits).

Am I wrong in thinking implementation would be much simpler without type inference?

Re:Hindley-Milner Type Inference (2012) by miloignis on Friday July 10, 2020 @04:19PM (#23795964)
I think it depends on the algorithm & implementation, but in at least some cases type inference adds very little code over type checking. (To some degree, both are propagating and unifying types through the program and throwing an error when trying to unify two things that aren't the same. And depending on the language, you've got to have some non-trivial machinery for handling function overloads, which can require inference-y reasoning anyway (like calling an overloaded function immediately with the result of another overloaded function).)

On the other hand, non-local inference can make things more complicated in some situations, depending on how hard you try. For instance, Rust gives up if you don't know the type of a variable and you try to accesses a member of it, where I once wrote a more complicated iterative solver that would see if it could infer the type of the variable from later context and return to the member access later. For example, consider the following Rust code where the compiler could figure out the types with some additional complexity but doesn't bother:

  fn main() {
      let a = (1..10).collect();
      println!("{:?}", a.len());
      let b: Vec<i32> = a;
  }

  error[E0282]: type annotations needed
Re:Hindley-Milner Type Inference (2012) by chriswarbo on Friday July 10, 2020 @05:55PM (#23796999)
> To some degree, both are propagating and unifying types through the program and throwing an error when trying to unify two things that aren't the same

In fact they're so similar that they can be combined to give 'bidirectional typing'. This is mostly useful for systems that are more expressive than Hindley Milner. For example, dependently-typed languages like Agda and Idris require type annotations for top-level definitions (local definitions can sometimes be inferred). Bidirectional typing is a neater approach to such hit-or-miss inference approaches, and also allows values to be inferred (your function-overloading example is a case of value inference: using the types to figure out which particular function to call).

Re:Hindley-Milner Type Inference (2012) by frank2 on Friday July 10, 2020 @07:23PM (#23797776)
>Rust gives up if you don't know the type of a variable and you try to accesses a member of it, where I once wrote a more complicated iterative solver that would see if it could infer the type of the variable from later context

My guess is that Rust's designers would reply that their compiler is slow enough without the complication you describe.

Re:Hindley-Milner Type Inference (2012) by IggleSniggle on Friday July 10, 2020 @04:45PM (#23796255)
Your comment has me feeling quite bullish on TypeScript type inference
Re:Hindley-Milner Type Inference (2012) by gnulinux on Friday July 10, 2020 @05:27PM (#23796702)
HM is pretty easy implement actually. You just need a unification function, the rest is just a laundry list of rules available everywhere, like Wikipedia. Source: I implemented a typed python dialect as a college project that used HM.
Re:Hindley-Milner Type Inference (2012) by eru on Saturday July 11, 2020 @12:31AM (#23799520)
> How much simpler would OCaml or Standard ML be if there were no type inference? I'm perfectly happy to never use type inference (maybe only for local variables).

Just the opposite: type inference forces the type systems to remain simpler. Without type inference you could make them more complicated.

Re:Hindley-Milner Type Inference (2012) by shpongled on Friday July 10, 2020 @05:13PM (#23796526)
Implementation would be simpler, but only by a small degree. To be honest, HM is relatively straightforward to actually implement.
Re:Hindley-Milner Type Inference (2012) by aspaceman on Friday July 10, 2020 @04:23PM (#23796014)
There's some really cool programming language theory that makes things like type inference easier than you think. It's still hard but there's rules and math and stuff for all of it.
Re:Hindley-Milner Type Inference (2012) by yawaramin on Saturday July 11, 2020 @01:24AM (#23799748)
You can't actually have a statically-typed language without type inference. Otherwise if you tell the compiler `(5 + 2) : String`, how would it report a type error?
Re:Hindley-Milner Type Inference (2012) by tomp on Saturday July 11, 2020 @02:11AM (#23799984)
That’s not type inference, just type propagation - tons simpler.
Re:Hindley-Milner Type Inference (2012) by yawaramin on Saturday July 11, 2020 @01:14PM (#23803709)
Sorry but what's the difference? Inference is a way to infer the type of any given expression. How does type propagation contrast?
Re:Hindley-Milner Type Inference (2012) by tomp on Saturday July 11, 2020 @06:27PM (#23806694)
Propagation is way easier, and basically just requires tracking types of values & parameters. To infer the type of `a` in

    var a = 1;
I just need to figure out the type of `1`, then realize that the type of `a` is the same.

slightly harder would be typing union / intersection types - e.g. this:

    var a = some_test() ? "foo" : new Bar();
Java would "infer" (kind-of) the type of `a` to be something like `Serializable & Comparable<? extends Serializable & Comparable<...>>` as both `Serializable` and `Comparable` are supertypes of both `String` and `Bar` (well, depends on `Bar` of course). This starts to show how much trickier things can get.

Now, imagine knowing nothing about what can be stored in the variable, and only seeing how it's used - this is the case for function parameters.

    (x) -> x + 1;
Is `x` a `float`? An `int`? A `long`? Maybe a vector? Basically, imagine the most complex type you can write in a given language, and then consider, the compiler would need to be able to prove that this is the only (or at least the most specific) type you could use.
Re:Hindley-Milner Type Inference (2012) by yawaramin on Saturday July 11, 2020 @06:46PM (#23806831)
Hmm, all of your examples can be inferred by Hindley-Milner. Of course, for the second one it would be a type error assuming that `Bar` is not a subtype of `String`. I'm not seeing how type propagation of a complex expression could be any simpler or easier than full-fledged type inference of that expression. For example,

    let x = "foo" |> bar |> baz 5
What's the type of `x`? You might say that we can figure it out by propagation, but how's that different from inference?
Re:Hindley-Milner Type Inference (2012) by tomp on Sunday July 12, 2020 @04:44AM (#23809912)
Indeed, what I'm saying is that inference is more powerful (i.e. better) than propagation. Most compilers only implement propagation, not full inference, allowing programmers to skip type annotations on local variables (since their types can be propagated from parameters and expressions), but not on parameters.

Basically, there's two complications with type inference. One is type variables that you need to then "solve" - in my last example above, the type of parameter `x` would start as a type parameter α (usually denoted by Greek letters in type systems literature), then it would be "unified" with `int` when used "like" an int (i.e. passed to a function `+ : (int, int) -> int`).

This is what happens in a language with a "simple" type system like OCaml, with no subtyping. (OCaml is slightly more complicated, but still all equations that it needs to solve only involve equality (I think)). If your type system features subtyping, things get a bit more complidated.

    (x) -> new List(x + 1, concat("test", x))
This now generates two constraints, `α <: int` and `α <: string`, that the compiler needs to solve. One solution would be an intersection type `string & int`. Does this type make sense? Does it even exist? Will the programmer understand it? The question isn't only finding the solution, but also which solution is "the best" - how to even define "the best"? I'm not aware of any good answers to this question (not even MLSub, it only works for simple cases).

The other big issue type inference deals with, is generalization. If, at the end of inferring an expression's type, there are no equations involving a type variable (a bit of a simplification, but let's go with it), then it can be "generalized" - turned into polymorphic type.

    (x) -> new List(x)
We might be able to assign the type `forall α . α -> list[α]` (or, a this would be written in Java / C++, `<T> T -> List<T>`), but again this depends on multiple conditions (is `x` used elsewhere? does it escape the scope? is `List` mutable?), and can get much more complicated with more features (e.g. upper and lower bounds, like Java's `<U extends Number>`)

In your example, figuring the type of `x` is trivial compared to above - just take the return type of `baz`.

Re:Hindley-Milner Type Inference (2012) by throwaway894345 on Friday July 10, 2020 @05:06PM (#23796454)
Local variables seems to be the sweet-spot for me, especially when I'm working on a project with others.
Re:Hindley-Milner Type Inference (2012) by eru on Saturday July 11, 2020 @12:21AM (#23799462)
Yes, the main reason to give type annotations is to make debugging easier and as documentation.

In eg Haskell, you often write the types first, even though in principle you seldom would have to write the types at all.