(The panda doesn't feel well, because of programming too much OCaml. But trust me on this, you do not want to see a panda that had to code C++)
OCaml is a very big and complex language, like C++, and many parts ofit kinda stick out, are experiments that didn't go well, or exist only for backward compatibility.
If you want to code OCaml, you might find these hints useful.
- "Exceptions" in OCaml won't give you a backtrace. This is easily one of the top 3 worst things about OCaml. So even in situations when you'd use an exception in Ruby/Python/Java/etc., think whether you can do something else in OCaml.
- For handling "can't happen" situations use something like failwith (sprintf "Internal error: function foo(%d, %s, %s) expects a non-empty list" a b (foo_to_string c)). Because you won't get a backtrace, you should include as much information as possible.
- OCaml's standard library is one of its weakest parts. Always use extensions to the std library. If you are concerned about people introducing unnecessary dependencies, simply copy sources of it to your repository with an appropriate copyright notice.
- As we know from Perl/Ruby/Python/every other modern language, the three most common data structures you ever use are strings, hash tables and resizable arrays. You can get OCaml support all of them reasonably well, but it doesn't by default.
- sprintf is one of the most useful OCaml functions. And do open Printf in all your OCaml files, it will save you a lot of typing and functions from Printf module have names that don't collide with anything. If you need to print something for debugging, define converters of all your types, like foo_to_string foo = sprintf "%s %d %s" foo.a foo.b foo.c
- DynArray (resizable arrays) is one of the most useful OCaml data types ever. No more idiocy like building list ref and reversing it.
- OCaml Hashtbl is weird and confusing. It is used for 1->1 hashes and 1->N hashes (and ('a,unit) hash tables are used as sets) and it's easy to get something wrong (was Hashtbl.add meant to add another element to 1->N hash or was it simply setting a value that didn't exist before ...). Besides, calling Hashtbl.blah is extremely verbose and it's impossible to include them in your namespace due to collisions. So it's very good idea to make types ('a,'b) ht = HT of ('a,'b) Hashtbl.t for 1->1 hash tables, ('a,'b) mht = MHT of ('a,'b) Hashtbl.t for 1->N hash tables and 'a set = SET of ('a,unit) Hashtbl.t. Then define functions like like ht_set, ht_get, mht_add, mht_get_all, ht_iter, mht_iter_all, set_add, set_mem etc. in a module that you include from all your files (I usually call it util.ml). And definitely define functions like ht_keys, ht_values, ht_iter_keys etc. I have no idea how could they have forgetten them in the std library.
- Do not use tuples bigger than 2 (in special cases 3) elements, or long-living tuples. Use records instead. Records can be easily extended, can be made mulable, and you won't have to remember which field of the tuple meant what. This also applies to Python ;-)
- Encapsulate all common folds and tail recursions. It's very easy to make a mistake with them and the code looks ugly. On the other hand high-level functions like map, iter, filter, collect, join etc. don't clutter your code. Never use the same recursion/folding pattern twice. Extract the pattern. I think it's a good idea to separate the pattern from the application code for single-use patterns too. Some examples: collect : ('a'->'b option) -> 'a list -> 'b list, mht_iter_all : ('a' -> 'b list -> unit) -> ('a,'b) mht -> unit.
- ocamldep is pretty helpful for writing Makefiles. Simply do ocamldep * >>.deps from time to time and include .deps from your Makefile.