The best kittens, technology, and video games blog in the world.

Showing posts with label ocaml. Show all posts
Showing posts with label ocaml. Show all posts

Tuesday, May 16, 2006

OCaml programming best practice





(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.

Wednesday, May 03, 2006

Variant types in OCaml suck

So, here's the first of the promised long, boring, technical rants. My MSc thesis is a compiler for shaders written in the RenderMan Shading Language for SaarCOR hardware. Writing a compiler is a pretty straightforward task - gather some tests, write a hardware emulator, then a parser, a middle end, a code generator, then play with the components until they produce something satisfactory. Not much of a blogable material. The first issue however, choice of the programming language, was a bit interesting. The options I considered were:

  • Java + possibly some high-level JVM-targeted scripting language
  • OCaml, used very conservatively
  • OCaml, used with all the fancy stuff like variant types
  • Other functional language, like Common Lisp or Scheme
  • Some real high-level language like Ruby, Perl or Python
OCaml's (and SML's) static typing is usually way too annoying. The reason I decided to try OCaml anyway were all the fancy things that it has and the older MLs didn't, the stuff which supposedly makes coding much easier, like objects, variant types, camlp4 and so on. That was kinda the last chance I was giving it, not quite satisfied with OCaml, but not yet willing to abandon it. And the way the fancy features are implemented really sucks. Today, let's talk about suckiness of the variant types. Imagine there are two types, flt_temp = `FLT_TEMP of int (floating point temporaries), and vec_temp = `VEC_TEMP of int (vector temporaries). There is also a type any_temp = [flt_temp|vec_temp] for temporaries of either kind. So in contexts where only float temporaries are allowed (like division), flt_temp is used, in contexts where only vector temporaries are allowed (like dot products) - vec_temp, and in contexts where any temporary is ok (like function argument) - any_temp. So far so good, and a dumb wrappers like F of flt_temp|V of vec_temp wasn't needed to implement any_temp.
let any_temp_to_string : [<any_temp] -> string
= function
|`FLT_TEMP(x) -> sprintf "t%d:float" x
|`VEC_TEMP(x) -> sprintf "t%d:vector" x
Notice the <. It means that any subtype of any_temp can be argument of this function. So it's possible to run any_temp_to_string on any_temp, flt_temp or vec_temp values, what's a really nice improvement compared to the old MLs. However at this point you may be a little bit confused - why is < needed ? Isn't a value of type flt_temp/vec_temp also any_temp ? Now that's where we get to the suckiness part, because it's not and it needs an explicit typecast. Unfortunately the typecast often needs to be repeated many times, because OCaml will report a type error without even looking at our annotations: For example, one might have thought that the following would work:
let extract_used_temps : any_comp -> any_temp list = function
|`DIVISION(a, b) -> [a; b]
|`DOT_PRODUCT(a, b) -> [a; b]
But it won't. The "correct" rewrite is (extremely ugly and verbose):
let extract_used_temps : any_comp -> any_temp list = function
|`DIVISION(a, b) -> [(a :> any_temp); (b :> any_temp)]
|`DOT_PRODUCT(a, b) -> [(a :> any_temp); (b :> any_temp)]
A related issue is that polymorphic functions of type 'a->whatever get instantiated to any_temp->whatever, not [<any_temp]->whatever. So Hashtbl.find ht_of_any_temps some_flt_temp is going to fail unless we use (some_flt_temp :> any_temp). The "let's screw the type system" function Obj.magic (identity function of type 'a->'b) every here and there limits the typecast bloat a bit, but I've found that it helps in maybe 10% of cases, and of course then, if you make a typo, you end up with random segfaults (not even a nice runtime exception like in a dynamically typed language). In the 90% of cases there isn't really a way to use Obj.magic, mostly because it's applied too late, only after OCaml finds what it thinks is a type error. Now some statistics - the compiler itself at the moment has 4239 lines, and that includes 175 uses of :> and 32 uses of Obj.magic. It's so ugly :-/ So in the end the type system is mostly something to fight with, not something that helps, just like it was so often the case in plain OCaml/SML. So for the next project, I'll try something with fewer types. And I'll probably give up on OCaml. Oh, and I also had some problems with OCaml's object system, maybe I'll blog about it sometime later. ^_^