http://www.podval.org/~sds/ocaml-sucks.html
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!).
- Static type checking
- No Macros
- Minor Language Suckiness
- OCaml Implementation Sucks
- Standard Library Sucks
- OCaml Language Rocks
- Caveats
- 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 float
s? 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
- the bizarre rules to decide how many expressions after
else
belong to it - the extra parens necessary to write negative numbers because
-
is interpreted as a function, not a part of a token; e.g.,List.fold_left (fun x y -> if x > y then x else y) -1 nums ;; This expression has type int -> int list -> int but is here used with type int
- there are actually three mildly different syntaxes:
- the official one is described in the manual
- a superset thereof is accepted by the compiler
ocamlc
- something similar (but ever so slightly different) is accepted by the preprocessor
Camlp4
(e.g., it acceptsList.map [1;2;3] ~f:fun x -> x
, which is also accepted by the top-level, but not the compiler)
- the bizarre rules to decide how many expressions after
- No Polymorphism
- Cannot add an
int
to afloat
- 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 thatprinterf
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).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); ]
Heregood
works whilebad
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 noArray.map2
. There areArray.mapi
andArray.iteri
, but noArray.fold_lefti
. Somehow::
is a syntax, not an infix version of the (nonexistent!) functionList.cons
. - No dynamic variables
- Without dynamic variables for default values, optional
~
arguments are next to useless. - Optional
~
arguments suck -
- The default values of the optional arguments cannot depend on positional arguments because of the calling conventions: to make function call delimiters optional, every function call for a function with optional arguments is required to end with a positional argument.
Values do not match: val foo : ?arg:string -> unit -> unit is not included in val bar : unit -> unit
even thoughfoo
can be called exactly likebar
, so one has to use(fun () -> foo ())
instead offoo
.- The default values of optional arguments restrict the possible type the argument may take. E.g., in
let max_len ?(key=(fun x -> x)) strings = Array.fold_left ~init:0 strings ~f:(fun acc s -> max acc (String.length (key s)))
the type ofkey
isstring -> string
, not'a -> string
as it would have been if the argument were required.
- Partial argument application inconsistency
- If
f
has type?a:int -> b -> c -> d
thenf b
has typec -> d
, not?a:int -> c -> d
as one might have expected from the fact that one can writef b ~a:1 c
. Moreover, typesa:int -> b:int -> unit
andb: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, functionsfoo
andbar
compiled whilebaz
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
, andint64
(and no automatic type conversion!) Additionally, functions take a fixed number of arguments, so, to multiply three numbers, you have to callInt64.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 youopen 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
andB
both definet
(a very common type name!), and you open both modules, you are not warned that one oft
's is shadowed. Moreover, there is no way to figure out in the top-level which module defines a specific variablefoo
. - 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 beforeg
. 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 anyx
) 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 runEmacs
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 in CLISP takes 150kb of RAM, in OCaml - 900kb, apparently, because some intermediate strings used for sexp parsing are never released (despite major collections and compactions). Another example is:
module M : sig val x end = struct let x = 7 let s = String.make (1024 * 1024 * 256) ' ' end
Here the huge valueM.s
cannot be accessed but is never collected.
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 correctlet round x = int_of_float (floor (x +. 0.5))
- Lists
-
List.map
is not tail-recursive!- Lists are immutable, thus cannot be spliced - non-consing operation is impossible.
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 onprintf "%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
- Make no mistake:
Java/C/C++/C#/Perl
are much worse thanOCaml
! There appears to be just one good tool, withOCaml
coming a distant second. - I know a few very smart people who love
OCaml
, so my personal opinion thatOCaml
sucks does not mean that you will not love it. - One 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 someLisp
andC
(or, better yet,Assembly
) exposure.
Relevant links
'Programming' 카테고리의 다른 글
(Racket) 문자열 개수 세기 (0) | 2012.09.20 |
---|---|
More: Systems Programming with Racket (0) | 2012.09.20 |
Continuation-passing style (CPS) (0) | 2012.09.17 |
Semicolons in JavaScript are optional (0) | 2012.09.15 |
(Clojure) Fibonacci Numbers (0) | 2012.09.14 |