OCaml Language Sucks

I have been using OCaml at work for a year now, so here are some reasons why I would not want to use it for software development (but see caveats!).

  1. Static type checking
  2. No Macros
    1. Wrappers
    2. Places
  3. Minor Language Suckiness
  4. OCaml Implementation Sucks
  5. Standard Library Sucks
  6. OCaml Language Rocks
  7. Caveats
  8. Relevant Links

Static type checking

False Sense of Security

Static type checking helps detect quite a few errors, which is very good. Alas, this may lull you into a false sense of security ("it compiles, so it must work correctly"), especially after you spend hours trying to satisfy the compiler (think of higher order functions taking higher order functions as arguments - see below for a simple example).

Few Basic Types

The bigger trouble is that to always be able to determine the type of every expression, OCaml has a very poor type zoo, e.g., no matrices, only one floating point type etc. CMUCL compilation notes can detect more subtle type problems than those detected by OCaml (e.g., argument falling outside an integer range).

Silent Integer Overflow

The biggest trouble is that integer overflow is not detected even at run time, which places OCaml right next to C and assembly with respect to type checking. One may argue that this is not a type issue (and you might even be right technically), but my retort is that the mathematical integers do not overflow, and I expect a language to provide something at least somewhat resembling them (and no, I cannot always use bignums - they are slow and cannot be used directly in arithmetics, see below).

Module Immutability

Another subtle issue (which cuts both ways, of course) is that you cannot easily modify the behavior of a module outside of it. E.g., suppose you have a Time module which defines and extensively uses Time.date_of_string which parses the ISO8601 basic format ("YYYYMMDD"). Suppose you need the full power of the module, but recognizing the ISO8601 extended format ("YYYY-MM-DD"). Tough luck: you have to get the module maintainer to edit the function Time.date_of_string - you cannot redefine the function yourself in your module. Semantically, OCaml compiles all functions INLINE (functions defined in functors are an exception to this rule, of course).

Polymorphism Causes Run-time Type Errors

Another not-so-subtle issue is that sometimes you get run-time type errors for no good reason. E.g., this code:

let f x =
  let t = Hashtbl.create 0 in
  x = t
compiles without error or warnings, but fails at run time with (Invalid_argument "equal: functional value") because it turns out that OCaml cannot compare hash tables (actually, it is not clear what prevents it from comparing hash tables, but this is another issue, a mere library bug). Clearly OCaml can issue a warning here, just like any Lisp will warn when compiling (1+ NIL), but it does not.

No Macros

The conspicuous absence of macros cannot be obscured by the presence of preprocessors.

Wrappers

What is the replacement for WITH-OPEN-FILE?

let call_with_open_input_file name func =
  let ic = open_in name in
  let res =
    try func ic
    with exn -> close_in ic; raise exn in
  close_in ic;
  res

(Do not forget the matching call_with_open_output_file!)

Now, how many OCaml programmers actually write their code this way? How many of them simply forget to close their channels? (This is a common hard-to-detect error because the OS closes the channels on program termination, so the problem rarely manifests itself). How many of them implement their very own UNWIND-PROTECT? How many do that correctly?

Places

Another area is places (AKA generalized references). OCaml offers incr to increment a reference to an int. How about floats? int64? Array elements? Mutable record fields? You have to write everything in full. Instead of my_record.my_field += increment (C) or (incf (record-field my-record) increment) (Lisp) you write my_record.my_field <- my_record.my_field + increment.

Note that C offers syntactic sugar (+=) while Lisp uses a regular macro (setf) which allows one to define new places. OCaml has a separate function := for references and syntax <- for arrays and records (yes, OCaml has to handle these situations separately!).

Minor Language Suckiness

No doubt the following behavior has some reasons behind it (I do not presume the OCaml creators to be malicious) but the reasons I heard so far indicate bad design. Note that OCaml is a new language designed from scratch, so the authors were not constrained by backwards compatibility considerations (that plagued the Common Lisp standardization process and imposed some ugliness on it), the mess we have stems from "new" (as opposed to "legacy") mistakes.

Record field naming hell
Cannot have two record types in the same file that have a field with the same name (and before version 3.09, you could not have a record field named contents!)
Syntax
Pretty much unreadable; especially
No Polymorphism
Cannot add an int to a float - need an explicit cast. This is an obvious deficiency, but there are more subtle ones. E.g, the following code does not work:
type t = MyInt of int | MyFloat of float | MyString of string
let foo printerf = function
  | MyInt i -> printerf string_of_int i
  | MyFloat x -> printerf string_of_float x
  | MyString s -> printerf (fun x -> x) s
because the first statement makes OCaml think that printerf has type (int -> string) -> int -> 'a instead of the correct ('a -> string) -> 'a -> 'b. (Yes, there are many workarounds, which make the problem look even more ugly - like the old Perl problem of "how do I create an array of file-handles").

In general, this type collapse makes it impossible to use polymorphic functions with higher order functional arguments (like printerf above) with variant types.

Another example (trimmed down from actual production code which had to be rewritten):

type 'a t1 = { s1 : int; s2 : 'a; }
let make f = f
let bad = [ make (fun x -> x.s1); ]
type a = { a : int; }
let good = [ make (fun x -> x.s2.a); ]
Here good works while bad does not. (to add insult to injury, it works in the top-level, but cannot be compiled in a file).
Inconsistent function sets
There is List.map2 but no Array.map2. There are Array.mapi and Array.iteri, but no Array.fold_lefti. Somehow :: is a syntax, not an infix version of the (nonexistent!) function List.cons.
No dynamic variables
Without dynamic variables for default values, optional ~ arguments are next to useless.
Optional ~ arguments suck
Partial argument application inconsistency
If f has type ?a:int -> b -> c -> d then f b has type c -> d, not ?a:int -> c -> d as one might have expected from the fact that one can write f b ~a:1 c. Moreover, types a:int -> b:int -> unit and b:int -> a:int -> unit are incompatible even though one can apply functions of these types to the exact same argument lists! Moreover, until 3.10, functions foo and bar compiled while baz did not:
let foo l = ListLabels.fold_left ~init:0 ~f:(fun s (_,x) -> s + x) l
let bar = (fun l -> ListLabels.fold_left ~init:0 ~f:(fun s (_,x) -> s + x) l)
let baz = ListLabels.fold_left ~init:0 ~f:(fun s (_,x) -> s + x)
Why? Actually, the latter is a good sign: Ocaml is improving!
Arithmetic's readability
Lisp is often blamed for its "unreadable" representation of arithmetic. OCaml has separate arithmetic functions for float, int, and int64 (and no automatic type conversion!) Additionally, functions take a fixed number of arguments, so, to multiply three numbers, you have to call Int64.mul twice. Pick your favorite:
(/ (- (* q n) (* s s)) (1- n))
(q * n - s * s) / (n - 1)
(Int64.to_float (Int64.sub (Int64.mul q (Int64.of_int n)) (Int64.mul s s))) /. (float n)
The above looks horrible even if you open Int64:
(to_float (sub (mul q (of_int n)) (mul s s))) /. (float n)
which is not a good idea because of silent name conflict resolution. An alternative is to define infix operators:
let ( +| ) a b = Int64.add a b
let ( -| ) a b = Int64.sub a b
let ( *| ) a b = Int64.mul a b
(Int64.to_float ((q *| (Int64.of_int n)) -| (s *| s))) /. (float n)
but this comes dangerously close to the horrors of "redefining syntax" (AKA "macro abuse") while not winning much in readability.
Silent name conflict resolution
If modules A and B both define t (a very common type name!), and you open both modules, you are not warned that one of t's is shadowed. Moreover, there is no way to figure out in the top-level which module defines a specific variable foo.
Order of evaluation
The forms are evaluated right-to-left (as they are popped from the stack), not left-to-right (as they are written and read). E.g., in f (g ()) (h ()) (that's (f (g) (h)) for those who hate the extra parens), h is called before g. This is done for the sake of speed and because in the pure functional world the evaluation order is inconsequential. Note that OCaml is not a pure functional language, so the order does matter!
No object input/output
Top-level can print any object, but this functionality is not available in programs. E.g., in Lisp one can print any structure to a file in a human-readable (and editable!) form to be read later. OCaml has to resort to cumbersome external libraries (like Sexp) to accomplish something like that. Compare Lisp:
(with-open-file (f "foo" :direction :output)
  (write x :stream f :readable t :pretty t))
with Ocaml:
call_with_open_output_file "foo" (fun f ->
  output_string f
    (Sexp.to_string_hum
       (sexp_of_list (sexp_of_pair sexp_of_foo sexp_of_bar) x)))
Note that OCaml has more parens than Lisp! Note also that the Lisp code is generic (it will write any x) while OCaml is only for an association list - you have to write a separate function for every type you save. This lack of pre-defined generic printing also complicates debugging (nested structures are hard to print) and interactive development.

OCaml Implementation Sucks

As all languages defined by their unique implementations, OCaml the language inherits all the warts of OCaml the implementation.

Compiler stops after the first error
Compiler should process the whole compilation unit (normally, a file) unless a total disaster strikes. E.g., if an expression has the wrong type, just assume the type is right and proceed to the next expression. You do not have to generate the executable, just report as many errors as possible!
No stack trace for natively compiled executables
If you want backtraces, compile to bytecode and sacrifice speed. (3.10 "fixes" this problem - an incomplete and usually incorrect and useless backtrace is available even for natively compiled executables).
Debugger sucks
Code running under debugger is so slow as to be useless. (Cf. gdb: I run Emacs under it at all times.) This lossage is mitigated by the presence of the top-level (REPL) though.
GC sucks
Loading the same data set grows a CLISP process by 150kb, and OCaml by 900kb; apparently, because some intermediate strings used for sexp parsing are never released (despite major collections and compactions) - or, if they are "released", they cause memory fragmentation (despite compactions?) which make the memory unusable. Other systems suffering from this problem include R. Another example is:
  module M : sig
    val x
  end = struct
    let x = 7
    let s = String.make (1024 * 1024 * 256) ' '
  end
Here the huge value M.s cannot be accessed but is never collected.
No implicit forward declarations
If declarations are recursive, this must be explicit:
   type t = s * s
   and s = ...
the more obvious
   type t = s * s
   type s = ...
does not work.

Standard Library Sucks

I think these flaws illustrate the prevalence of the implementer perspective in the design of OCaml as opposed to the user perspective.

Function round is absent
This seems like a small fish until you realize that many people implement round incorrectly like this:
let round x = truncate (x +. 0.5) (* BROKEN! *)
(forgetting about the negative numbers) instead of the correct
let round x = int_of_float (floor (x +. 0.5))
Lists

OCaml Language Rocks

For balance, I must mention some points where OCaml wins:

Format control checks
Getting an error on printf "%d" 3.14 is very nice. Getting an error on printf "%f" 3 is not so nice.
OMake
Automatic dependency detection is very nice (note that omake is not tied to ocaml, it can be used for any project, also, you pay for the automatic dependency detection with speed). Also, file comparison is done by contents, not mtime, so adding a comment results in recompilation of just the modified file, not its dependencies. Also, the upcoming 3.10 comes with ocamlbuild which is allegedly similar to asdf. Alas, the current (3.09) ocamldep sometimes misses some dependencies (well, all software has bugs).
Comment syntax
Nested comments are nice
Speed
The generated code is quite fast (as long as you are willing to forgo all polymorphism and stack traces). The OCaml developers have made speed their top priority and have sacrificed a lot to speed up the compiler and the generated code. They did mostly succeed, although quadratic algorithms make large (usually generated) polymorphic variant types completely unusable (hours of time and gigabytes or RAM for compilation!)
Pattern matching
This is a very powerful tool, should be easily implemented as a Lisp macro using DESTRUCTURING-BIND.
Functors
A nice feature which somewhat mitigates the limitations imposed by static typing and lack of macros.

Caveats

  1. 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.
  2. I know a few very smart people who love OCaml, so my personal opinion that OCaml sucks does not mean that you will not love it.
  3. You should try OCaml (or, better yet, Haskell) even if you think it sucks and you are not planning to use it. Without it, your Computer Science education is incomplete, just like it is incomplete without some Lisp and C (or, better yet, Assembly) exposure.

Relevant links