57 comments

[ 4.9 ms ] story [ 101 ms ] thread
Agree with many of these, but

>Cannot add an int to a float - need an explicit cast. This is an obvious deficiency

is ridiculous. Implicit conversions cause huge classes of errors in languages that support them. There's not even well-defined behavior for converting an integer that won't fit in the mantissa to a float.

My experience with OCaml is that it made too many concessions to imperative programming to be a particularly useful functional language and its type system is sorely lacking. Things like 31/63-bit integers for pointer tagging, binary "generic" comparison operators, ;-sequencing, etc. are terribly ugly hacks.

I agree that the issues you mentioned are annoying warts for Ocaml but I'm not so sure that they are all concessions to imperative programming. The 31-bit ints have more to do with simplifying the garbage collector implementation. The "generic" comparison operators have more to do with the lack of type classes. This has a bit to do with the imperative bits of the language (which make implementing type classes a bit harder) but also has to do with conflics with the existing module system (ocaml Functors support type encapsulation and some other things that make it harder to add type classes to the language)
The rather idiosyncratic language preferences of the author show through at the end:

> Make no mistake: Java/C/C++/C#/Perl are much worse than OCaml! There appears to be just one good tool, with OCaml coming a distant second.

where "one good tool" links to http://www.podval.org/~sds/tool.html, an essay praising Common Lisp.

Devout Lisp afficianados tend to be a little... opinionated.
It does not matter how we put it. If one is wrong, they are wrong. If one is "opinionated" and he/she is wrong, they will fail.

It hasn't helped Lisp's cause that a majority of its promoters are heavily opinionated.

From https://en.wikipedia.org/wiki/Cromwell%27s_rule

> If the prior probability assigned to a hypothesis is 0 or 1, then, by Bayes' theorem, the posterior probability (probability of the hypothesis, given the evidence) is forced to be 0 or 1 as well; no evidence, no matter how strong, could have any influence.

This is true because

Devout ?x aficionados tend to be a little... opinionated.

I've met plenty of devout Prolog, C, C++, and Java aficionados, and yes, they're opinionated.

Also, you're more likely to notice opinions that differ from your own than ones you agree with, these are more likely to be held by groups you don't belong to, and you're less likely to belong to minority groups, such as Lisp programmers.

> I've met plenty of devout Prolog aficionados

No you haven't.

I can understand your incredulity, but yes, I have. They even called themselves "the Prolog mafia". This was in the early 1990s. I like Prolog, and regret that it's hardly used nowadays.
From that "tool" essay:

> (well, you cannot have garbage collection if you have pointers anyway)

what? Java and so on certainly have pointers. Was this written before anyone realized that was possible, or is he just defining "pointers" differently from how I am?

I think the author means raw pointers to memory addresses, rather than pointers to variables/objects.
Even then you can have a GC. It just has to be conservative and non-compacting. Even C/C++ have a GC via boehm-gc, a LIBRARY that implements garbage collection!

(This is why I love C. No other practical language can claim to be able to implement a full-fledged GC as a library.)

> (This is why I love C. No other practical language can claim to be able to implement a full-fledged GC as a library.)

Not to rain on anyone's parade, but I don't think many people think of a conservative & non-compacting GC as "fully fledged".

(Don't get me wrong. It's probably useful, but y'know... let's call a spade a spade.)

I fat-fingered a down vote on this; just apologizing here. The perils of browsing HN comment threads on an iPhone...
Modula, Oberon, Ada... All have been used to implement GC's and OS's.
What's the difference, in practice? Just the fact that you can do pointer arithmetic on the former?
GC is intractable with pointer arithmetic.

Edit: to be clear, this is the classic difference between a reference (a value without arithmetic) vs a pointer.

This isn't quite true, although you need some sort of guarantee on the arithmetic to ensure that each pointer is bounded within an object.

C, in theory, allows this restriction, since computing a pointer to another object and dereferencing it is undefined behavior, but most implementations seem to relax this restriction.

I think that Symbolics C did have this restriction.
> This isn't quite true, although you need some sort of guarantee on the arithmetic to ensure that each pointer is bounded within an object.

I mean, this isn't pointer arithmetic, this is constrained offset lookup implementing a subset of pointer arithmetic (only addition of numbers >= 0?). I'd argue they're still not pointers unless you can derefence an arbitrary integer.

Then you're arguing that strictly conforming C code does not have pointers.
The semantics of the english here is a little lacking, but I think strictly conforming C code has pointers that behave like references. To my current understanding, this is highly unrealistic, hence ameliorating tooling like address sanitization.

To be clear, they would still be called pointers. But when people talk about the complications of pointers, they are referring to non-standard behavior which is necessary with today's C code (and pretty much all historic C code).

That's not really true, at least not with C.

C doesn't permit arbitrary pointer arithmetic. e.g. If you take a pointer to the first element of an array and decrement it, the behaviour is undefined. It's permissible for the GC to crash your program in that situation. You don't need to dereference it: just having that pointer, even ephemerally, is undefined behaviour.

If you deallocate an object, then all pointers to that object are, technically, rendered as-though uninitialized: it is undefined what happens if you even attempt to determine the address of a previously-deleted object. This permits implementations to set dangling pointers to NULL.

Finally, a pointer to e.g. an int can't be used to access e.g. a float at that location. It is valid to convert an int pointer to a float pointer, but the consequences of dereferencing it are undefined. An exception exists for characters: any pointer can be converted to a char pointer and used to read the bytes of the object.

The upshot is that every pointer in C always points at a valid object of a known type - or just-allocated memory which hasn't had a value put in it yet - or the GC is allowed to crash the program. Of course, most (all?) compilers don't actually generate code where it's always possible to find out what type of object a pointer is pointing at, or even whether the pointer has actually gone out of range.

The real issue in C - other than a lot of code depends on the above rules not being enforced - is that it's valid to convert a pointer to an integer, store it for long enough for an eager GC to remove the object, and then convert that integer back to a pointer. Now you have an invalid pointer, but the standard doesn't allow the pointer to become invalid. That means the only valid GC must assume any integer might be a pointer in disguise (unless it can prove it could never be converted into a pointer), and it can't move objects around either, because it can't just go modifying integers which might be pointers to those objects.

Notably, this also prevents being able to implement functionality required for performant GC, like generational collection (as moving references is virtually impossible without controlling the runtime).
Haskell -XStrict, problem solved :P.
..until you need to use somebody's Mensa-level-obfuscated, undocumented, library. Life is too short.
Are you saying that doesn't happen in the OCaml ecosystem?
yes it does. Fair comment. But Haskell attracts the 1% CS elites who, sadly, are counterproductive to the language's wider adoption. Ocaml is not pure enough for these guys, and so, a paradoxically smaller community produces a higher amount of actually practical stuff that an imperative "dunce" like me can hope to aspire to using.

If I may put it another way, Haskell is too beautiful for its own good, because it encourages "art", not engineering.

As a Haskell User, ....yes I can totally be a non-productive artist, but I can also bite my tongue, disable the language extension, and get shit done too :).

Consider from other perspective. Yes there are some pretty crazy libraries out there, but I assure the more (in)famous are indeed used, and used practically by much of the people who do, let's say, work professionally in Haskell. Now maybe that does create an inner-circle trap that impedes broader adaption among developers, but the services/app/executables do get made.

The thing that bugs me about OCaml is many of the flaws are just so damn simple-stupid. Not module limitation, but == and friends, let's say (to use things from link). I respect the ML tradition, but I think Rust is going to eat OCaml's lunch on the pedagogy/developer-mind-share front.

You have just persuaded me to give Rust a try... I like the idea of an advanced programming language with a pedagogical bent, that still allows me to get shit done. Since you seem to be a functional guy, the fact that you respect Rust is a selling point for me.

EDIT: oops - no REPL in Rust. Big hurdle for a data-science guy like myself. Scala/Ocaml/Haskell: all have REPL. That's also a big impediment to the pedagogical angle.

Great! I am a functional programmer at heart. But I definitely have masochistic system programming tendencies, and Rust is a good outlet. I have high hopes for Rust to be the gateway drug to the world of good languages. I do which it was more powerful in some respects, but it's definitely slick. There's plenty of really head-shaking mistakes in OCaml that everyone will run into from day one, that, like the OP, I can't imagine why they were made in the first place. Rust doesn't have those.
It's a shame too because rust used to have a repl back in the day. I'm sure they had reasons to get rid of it, but it's still a shame. I understand there's some sort of initiative to write another one though, but I don't know what its status is.
-XStrict doesn't make Haskell's module system any less of a bad joke.
Yes, yes. I wish we had https://www.mpi-sws.org/~dreyer/papers/mtc/main-long.pdf too. http://plv.mpi-sws.org/backpack/ and http://www.lpw25.net/ml2014.pdf will bridge the gap between OCaml and Haskell from both sides, but I know of no plan for doing something about the rather embarrassing facepalms mentioned in the OP.

At least most of Haskell's facepalms are in the std library, not the language (monad fail, applicative monad), which makes there after-the-fact remedy slightly less herculean.

If I understand correctly, Backpack's design is primarily driven by compatibility considerations (“how do we build upon GHC's existing foundation?”), rather than elegance. In particular, Backpack doesn't eliminate those ugly .hs-boot files, it just automates and hides their generation and processing.

For all their faults, Standard ML and OCaml have pretty good support for modular programming. And, as the Modular Type Classes paper you linked shows, type classes can be built elegantly on top of a good modular foundation.

I agree that backpack leaves Haskell with too many overlapping concepts. But consider things could be unified internally, and compatibility-breaking language extensions introduced down the road. That's my hope at least :).

Also, check out https://www.mpi-sws.org/~rossberg/1ml/ . I think it makes a pretty good argument that there is a similar problem going on with the MLs versus the ideal too. (The overlapping features are also not a subset of Haskell's overlapping features, interestingly too.)

As far as I can tell, Standard ML doesn't have much of a feature overlap problem. Off the top of my head, the only redundant feature is `abstype`, which I think is a leftover from the 70's, when ML didn't have its module system. If Standard ML had first-class modules, then modules and records could also be considered redundant.

What Standard ML suffers from is lack of expressivity. The lack of (pattern) guards sometimes forces pattern matching blocks to (nest more than necessary and) contain redundant clauses. The lack of polymophic recursion forces non-uniformly recursive data types (as well as the functions that operate on them) to be rewritten in a way that makes the type checker a less effective bug-finding tool. Without higher-rank types, you can't express the Day convolution, free Applicatives, or Traversals (from the lens library). On the other hand, I don't find myself missing higher-kinded types much, since they are mostly useful in combination with type classes, which modules can adequately replace.

What 1ML does is unify features: (0) records and let expressions are now particular uses of modules, (1) core language functions and type constructors are now particular uses of functors.

> On the other hand, I don't find myself missing higher-kinded types much...

I don't find myself missing modules that much for precisely the inverse reason.

> If Standard ML had first-class modules, then modules and records could also be considered redundant.

If Haskell+Backpack had first class packages/modules, then packages/modules and type classes could also be considered redundant.

This is the duality I am trying to get it :). Looking each language alone, the features are "somewhat orthogonal". But in the context of the entire design space, the lack of unification comes off as redundancy.

> I don't find myself missing modules that much for precisely the inverse reason.

The “tiny” detail is that ML modules do much more than what type classes do: they allow you to define abstract types whose representation is invisible to the rest of the program, without ugly hacks like `newtype`-wrapping the internal representation.

> If Haskell+Backpack had first class packages/modules

Packages and modules are completely different things. A package is a unit of distribution, whereas a module is a unit of encapsulation. Even within a single program that's only distributed as a whole, you may want different components to hide their internals from each other.

> This is the duality I am trying to get it

There is no duality between modules and type classes, because, while modules can do everything type classes do (and more), the converse does not hold.

Remember, it's the combination of type classes and higher kinded types that the MLs are up against.

[Not because there is an inherent trade-off (because there isn't one between type classes vs modules either as the Dryer paper shows), but beause the MLs have that weakness.]

Non-recursive higher-kinded types can be implemented in terms of functors. For example, the free monad type constructor is very easy to implement in Standard ML (OCaml is similar):

    signature FUNCTOR =
    sig
      type 'a t
      val map : ('a -> 'b) -> 'a t -> 'b t
    end
    
    functor FreeMonad (F : FUNCTOR) =
    struct
      datatype 'a t = Pure of 'a
                    | Free of 'a t F.t
      (* operations go here *)
    end
Modules can do everything type classes can do, and more. There is no weakness.

The real reason why you need to define this as a higher-kinded type constructor in Haskell (`data Free f a = ...`) is to overcome the limitation of not having parameterized modules. The way it's done in Haskell also has the defect that the type system will happily allow you to construct a value of type `Free Foo a`, where `Foo` is not a functor, whereas the ML version prohibits this by construction.

That's not the same however, because the modules language is a lot more constrained than the term language. See the first paragraph the second page of https://www.mpi-sws.org/~rossberg/1ml/1ml-extended.pdf.
> That's not the same however, because the modules language is a lot more constrained than the term language.

You don't need to tell me. That's obvious to anyone who has programmed in ML. But Haskell's type class language is similarly a lot more constrained than its term language. It's basically a pure (in particular, cut-free) compile-time Prolog, with some restrictions to guarantee that no query will return more than one result.

> See the first paragraph the second page of https://www.mpi-sws.org/~rossberg/1ml/1ml-extended.pdf.

Rest assured, I've read the paper - not today, but several months ago. I'm quite familiar with the ML module literature. Check my post history:

https://news.ycombinator.com/item?id=10856201

https://news.ycombinator.com/item?id=11131265

But, since you're arguing that the combination of type classes and higher-kinded types can do something that ML modules can't, I'm providing evidence for the opposite: everything that can be done with type classes and higher-kinded types can be done with modules.

(0) You can't compute new modules at runtime, but you also can't define new type class instances at runtime.

(1) An ML module can have both transparent and abstract type components, controlled by the programmer. A type class always has abstract type components at definition time, and transparent type components at instantiation time. Modules are more expressive than type classes.

(2) The signature of an ML module is structural, and you can, after the fact, selectively forget part of a module's contents. Type classes don't let you do this. Try making `Monoid` a subclass of `Semigroup` without recompiling any existing code. Again, modules are more expressive than type classes.

As for higher kinds, at least with OCaml's higher-order applicative functors, you can encode arbitrary higher-kinded type constructors. And most of the higher-kinded constructors that actually arise in practice can be encoded even with Standard ML's first-order functors.

> Rest assured, I've read the paper

Don't worry, I quote the paper not because I doubt your ML experience, but to make sure that I don't mess up the peculiars of the situation. I've spent more time thinking about the design of SML/OCaml than using them, and wouldn't want to undermine my argument with a mis-remembered surface syntax.

My point with this arguments is not that "polymorphic instances" are as powerful as functors (they aren't!). Rather in this (and similar examples) it's higher-kinded types vs modules, no type classes involved. The Haskell equivalent is just a plane function, not a polymorphic instance, and there is things that can be done in the term language but not the module language.

SML/OCaml with higher-kinded types would annihilate this argument, no type classes or implicits needed, but don't yet exist.

> My point with this arguments is not that "polymorphic instances" are as powerful as functors (they aren't!). Rather in this (and similar examples) it's higher-kinded types vs modules, no type classes involved.

This paper disagrees: http://www.cse.unsw.edu.au/~chak/papers/modules-classes.pdf . See below.

> The Haskell equivalent is just a plane[sic] function, not a polymorphic instance, and there is things that can be done in the term language but not the module language.

A plain function can't return fresh new types. A functor can.

> SML/OCaml with higher-kinded types would annihilate this argument

As I said above, OCaml's higher-order modules let you encode arbitrarily higher-kinded types. Even if OCaml didn't have parametric polymorphism in the core language, you can express it in the module language with applicative functors (although the result would be a lot more verbose). If you don't believe me, give me an arbitrary higher-kinded type, and I'll tell you how to encode it in OCaml.

I don't have time to read the paper for a few days, but I meant while functors can do things functions cannot do, functions can do things functors cannot do too.
And records can do things functions can't do. And vice versa. That alone doesn't mean records in one language are the counterpart of functions in another.
How is that relevant? I'm claiming that their exists functions in Haskell that cannot be encoded either as functor or functions in ML. (If you want, I'll try to come up with an example, but my guess would be some funny polymorphic recursion + higher-kinded types)
(comment deleted)
Some of these points are a bit outdated. Ocaml improved a lot recently.

> False Sense of Security

This is a strawman argument. And the static types really do help, specially if you make good use of algebraic data types and the exhaustiveness checker.

The autocompletion and documentation suport is also super nice. Installing Merlin is one of my top priorities when I set up an ocaml environment.

https://github.com/the-lambda-church/merlin

> Few Basic Types

Nowadays you tend to see tons of different integer types in systems programming languages (C, C++, Rust, etc). This is more the author saying he loves Common Lisp than a downside of Ocaml.

> Module Immutability

Can't you define a new module that wraps around the old one and overrides the `date_of_string` function? Changing the implementation of a module violates all sorts of abstraction guarantees and would lead to many thorny issues.

> Polymorphism Causes Run-time Type Errors

Polymorphic equality and comparison can definitely be a source of confusion. Hopefully in the future we will have module implicits and be able to solve this issue similarly to how Haskell does it.

> No Macros

Again, this is more of a "my language is not LISP" complaint. Ocaml has camlp4, which is not a full-blown macro system but allows for a large variety of syntactic extensions to the language. However, camlp4 was tricky to maintain in sync with the original language so nowadays the community is moving towards simpler ppx transformers:

http://whitequark.org/blog/2014/04/16/a-guide-to-extension-p...

> Record field naming hell

This got much better in more recent Ocaml versions, which let you have multiple records with the same field name living in harmony.

> Syntax

I'll have to agree with this one. Ocaml definitely doesn't have the prettiest syntax.

> No (ad-hoc) Polymorphism

This is a price to pay for keeping the type system sane. More flexible arithmetic coercion rules would also get in the way of type inference, which is a really nice thing about Ocaml.

> Inconsistent function sets

Its a bit less awful if you use one of the "replacement standard libraries", such as Containers, Core or Batteries. But yes, the suckyness of the standard library is a huge pain point.

> Arithmetic's readability

I don't think defining custom operators is as bad as the author thinks here. Their precedence is defined by the symbols you use so a new multiplication operators should behave similar to all the other ones.

But yes, the monomorphic operators really are annoying. As I said before, I'm really looking forward to module implicits (sadly, it seems we'll have to wait at least until the version after the next one though)

> Order of evaluation

This one is even worse than he says. Order of evaluation is undefined and when compiling to bytecode its different from when its compiled to native code (one is left-to-right and the other is right-to-left)

> No implicit forward declarations

Being able to shadow existing definitions is pretty nice sometimes. Haskell does the "declarations are recursive by default" thing and it sometimes gets in the way.

> Standard Library Sucks

Can't argue against that. But at least there are replacement libs you can use to make it suck a little bit less.

> OMake

Nowadays I think people are moving towards ocamlbuild instead.

Mostly agree with your post, just a few remarks:

> Polymorphic equality and comparison can definitely be a source of confusion.

The problem is entirely with OCaml, since equality and comparisons aren't broken in Standard ML and Haskell.

> Again, this is more of a "my language is not LISP" complaint. Ocaml has camlp4, which is not a full-blown macro system but (...)

Lisp macros are totally broken too, since there's basically no way to protect any programmer-defined abstractions. (Not that one should expect much from Lispers in this regard.) This is how you do macros right: http://docs.racket-lang.org/syntax/Parsing_Syntax.html

Also, generally if you're doing a lot of math with floats (or some other numeric type) you can do:

    Float.(x + y * 2.0 - z / w)
or even just open the Float module locally

Also, the with-open-file problem can be solved pretty simply by using GADTs (and this makes it simple to differentiate between the different modes you can open a file in, as well).

A very enjoyable read! The author sounds like a curmudgeon of the first order. Unfortunately, my only take-away after exploring his website is the joke about the pessimist.