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 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).
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).
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).
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).
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 = tcompiles 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.
The conspicuous absence of macros cannot be obscured by the presence of preprocessors.
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?
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!).
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.
contents!)else belong to it- 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
ocamlcCamlp4 (e.g., it accepts
List.map [1;2;3] ~f:fun x -> x, which is also
accepted by the top-level, but not the compiler)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) sbecause 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).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.~
arguments are next to useless.~ arguments suckValues do not match: val foo : ?arg:string -> unit -> unit is not included in val bar : unit -> uniteven though
foo can be called exactly like bar,
so one has to use (fun () -> foo ()) instead of
foo.
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 of key is string -> string,
not 'a -> string as it would have been if the argument were
required.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!
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.
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.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!(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.As all languages defined by their unique implementations, OCaml the language inherits all the warts of OCaml the implementation.
gdb: I run Emacs under it at all times.)
This lossage is mitigated by the presence of the top-level (REPL) though. 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.type t = s * s and s = ...the more obvious
type t = s * s type s = ...does not work.
I think these flaws illustrate the prevalence of the implementer perspective in the design of OCaml as opposed to the user perspective.
round is absentround 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))
List.map is not tail-recursive!For balance, I must mention some points where OCaml wins:
printf "%d" 3.14 is very nice.
Getting an error on printf "%f" 3 is not so nice.ocamlbuild which is
allegedly similar to asdf.
Alas, the current (3.09) ocamldep sometimes misses some
dependencies (well, all software has bugs).DESTRUCTURING-BIND.Java/C/C++/C#/Perl are much worse
than OCaml!
There appears to be just one good tool,
with OCaml coming a distant second.OCaml, so my personal opinion that OCaml
sucks does not mean that you will not love it.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.| Sam Steingold<sds@podval.org> | created: 2007-01-31 |