Hacker News new | ask | show | jobs
by fusiongyro 4975 days ago
They're not worth discussing because they're a counterintuitive mess rather than a case study of the glory of HM. Maybe edge case isn't the right word, but they're definitely not something I would hail as a perfect resounding success.

There are no polymorphic literals in ML, just polymorphic math operators, which is enough of a blight on the standard that OCaml discarded it and forces you to use different operators for real and integer arithmetic. And there's only one kind of string in both MLs.

Haskell's Num hierarchy is troublesome. They traded usability for + with complexity for /. It's extremely unlikely that you could write a program in Haskell that does much arithmetic and have it build correctly on the first try without any manifest typing. This is one reason students of Haskell find things so confusing: type declarations are necessary at the top level simply because the extensions and complexity of modern Haskell break HM if you try using it everywhere. Also, the class system in there is not especially mathematically correct, which leads to the numerous replacement Preludes that try to do a better job but haven't caught on.

Strings are edge cases because they are not polymorphic unless you enable OverloadedStrings. Once you do, you will either replace the built-in string with something else (ByteString or Text) or find yourself in the same kind of trouble you'd be in with Num.

Let me be clear: I'm not saying that these problems are showstoppers. They're really minor annoyances once you're experienced, though they contribute to confusion for beginners. The point I'm trying to make is that you can't just drop HM into any old language and expect it to work. A greater point would perhaps be that all languages have warts simply because they're large, complex beasts (Haskell and C++ especially) and it's unproductive to point to a missing feature in one and demand some sort of perfected version of the other's.

1 comments

> They're not worth discussing because they're a counterintuitive mess

Do you really believe Num overloading is a counterintuitive mess? I disagree completely.

> OCaml discarded it and forces you to use different operators for real and integer arithmetic

Which is pretty terrible.

> Haskell's Num hierarchy is troublesome.

Yes, but that's an orthogonal issue.

> This is one reason students of Haskell find things so confusing: type declarations are necessary at the top level simply because the extensions and complexity of modern Haskell break HM if you try using it everywhere

That sounds like FUD to me, a heavy Haskell user. Type declarations at the top-level are generally necessary to avoid the dreaded MR and for documentation purposes. Modern Haskell doesn't heavily use extensions that require type annotations on the top-level.

> Strings are edge cases because they are not polymorphic unless you enable OverloadedStrings. Once you do, you will either replace the built-in string with something else (ByteString or Text) or find yourself in the same kind of trouble you'd be in with Num.

It lets me use literals for Lazy Text, Strict Text, and String with the same syntax, which is nice.

My point is merely that giving a type to a polymorphic initializer is possible, and C++ chose not to.

The distinction between your point and mine is becoming miniscule, but I must defend my position on Haskell, as a heavy user myself. If Num doesn't seem to be a problem to you, it's because you supply top-level annotations that disambiguate it. Try removing all the annotations from whatever you did last week and see if it still compiles cleanly. I'd wager it doesn't. This isn't an issue in practice because we supply annotations in most cases, but don't be fooled: top level annotations, despite the rhetoric, actually are essential for modern Haskell programs to disambiguate. As for strings, the problem doesn't appear in practice because people mostly don't intermingle string types in the same module. That's what makes Num tricky; it's easy to find yourself with Ints and Integers together, wanting to divide them and get a float, and figuring out how to resolve these minor issues is significant for learners. Just ask my friends!
This is one of the files in a project I've worked on last week.

https://github.com/Peaker/bottle/blob/master/codeedit/Editor...

I removed all the top-level type declarations, and only one definition broke, because of the MR.

  showP :: Show a => a -> String
  showP = parenify . show
Once I removed the type declaration, I made it work again by adding a parameter to avoid the MR:

  showP x = parenify (show x)
and everything compiles smoothly.

Feel free to browse the bottle repo -- and try to build it without top-level declaration. Apart from a few functions in the entire project that use Rank2, you won't need any declarations.

Thanks for sticking with this conversation.

This is very good code.

One difference I see between our styles that may explain the differences in behavior we see is that you're quite meticulous about importing only the parts of modules you need, and you make heavy use of qualified imports. My style has been to import everything in case I need it later and only use qualified imports when absolutely necessary, and it must be creating the unnecessary ambiguity that I have to deal with. I will try to adopt your style and see if it cleans up my error messages, and I'll encourage my friends to do the same.