OCaml Language Sucks (2007)
podval.org
podval.org
> 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 ?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.
No you haven't.
It hasn't helped Lisp's cause that a majority of its promoters are heavily opinionated.
> 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.
> (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?
(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.)
Edit: to be clear, this is the classic difference between a reference (a value without arithmetic) vs a pointer.
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.
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 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.
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).
>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.
> 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.
Float.(x + y * 2.0 - z / w)
or even just open the Float module locallyAlso, 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).
> 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
The simple example does not look 'simple' though.
> https://blogs.janestreet.com/extension-points-or-how-ocaml-i...
user calls it a 'bloody hack' and 'we are still far away from Lisp here.'
If I may put it another way, Haskell is too beautiful for its own good, because it encourages "art", not engineering.
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.
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.
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.
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.
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.)
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.
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.
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.
[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.]
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.
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.
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.
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.