I am a huge fan of the Advent of Code. I eagerly wait for it every year and once it starts I try to solve the problems the same day they appear. The nice thing about the problems is that they are initially relatively simple and get more difficult over time (and if you ever get stuck there is a subreddit for each problem that you can use to unblock yourself). This makes it a fantastic opportunity to try new languages. (Note, I intentionally said “try” and not “learn” because the vast majority of the problems can be solved in less than 100 lines of code and almost never require advanced data structures and/or language features). And this is what I do – each year I pick a language I have never used before and solve all the problems using this language. Two years ago – the first edition of the Advent of Code – I tried Scala, last year I tried Go and this year I ended up picking OCaml (with Rust and D being other contenders). One thing I decided to do differently this year however, was to write down my observations and things I struggled with to share them, hoping that it will help people trying to learn OCaml.
Preparing Development Environment
Before I could start solving the problems I needed to setup my dev environment and figure out how to build and run programs written in OCaml. I used Visual Studio Code as my editor because I was sure it would have an extension for OCaml (I used OCaml & Reason IDE). To build my programs I ended up using
ocamlbuild although probably it was an overkill – I never had more than one file to compile and used at most one additional library (
Str). With my dev environment set up I was ready to start solving Advent of Code problems and learning OCaml the hard (i.e. trial-and-error) way.
When you try to pick a new language, you will initially make a lot of simple, syntactic mistakes. I think this was one of the biggest barriers to me at the beginning. I would try to compile my program and the compilation would fail with an error I could only stare at. Understanding the error would initially take me a lot of time and experimenting (like commenting out code etc.). After a few days I figured what errors my most common mistakes resulted in and things got much easier, but I would still occasionally encounter an error which took a relatively long time to resolve. (Note, some of the mistakes could probably have been avoided if I had read more on the language before I started coding but this is not the way I learn – I prefer reading just enough to be able to do simple things and then figure out things as I go).
Before I dive deeper into errors I encountered I would like to give a few hints that can help in locating and understanding the error:
- The location of the error contains the line and the column where the error occurs. The column can be very helpful – especially when you invoke a function with multiple parameters or inline a function invocation
- Oftentimes the mistake is not in the line the error message points to. If you can’t find anything wrong with the line the error message points to check the line(s) the function is invoked from
- Learn how to read function signatures (e.g.
int -> int -> int) to easier understand errors caused by passing values of incorrect types. See the Types of Functions in the OCaml tutorial.
During my adventure with OCaml I compiled a list of errors I encountered.
This was initially the most common error I saw. It can have many causes and I am sure that the list below is not exhaustive. Most common causes:
inafter variable declaration:
let sum a b = let s = a + b print_int (* Error: Syntax error *) let sum a b = let s = a + b in print_int s (* val sum : int -> int -> unit = <fun> *)
- using reserved words as variable names (I fell a few times for
matchI wanted to use respectively for a variable storing a temporary value and a result of regular expression match)
->in pattern matching (the exact error is
Syntax error: pattern expected
This error means that the function you are trying to call cannot be found. Most common causes:
- You have a typo in the function name
- Your recursive function is not marked
let factorial n = if n = 0 then 1 else n * factorial(n - 1) (* Error: Unbound value factorial *) let rec factorial n = if n = 0 then 1 else n * factorial(n - 1) (* val factorial : int -> int = <fun> *)
This function has type X It is applied to too many arguments; maybe you forgot a `;'.
Syntax error there can be multiple causes for this error:
- You actually forgot a
;to separate your statements:
print_string "a" print_string "b" (* Error: This function has type string -> unit It is applied to too many arguments; maybe you forgot a `;'. *) print_string "a"; print_string "b" (* Result: ab *)
- You passed too many arguments to a function:
let sum list = List.fold_left (fun v a -> a + v) 0 list sum [1;2;3] "a" (* Error: This function has type int list -> int It is applied to too many arguments; maybe you forgot a `;'. *) sum [1;2;3] (* Result: 6 *)
- You forgot parenthesis when inlining a function (similar to a previous case but sometimes harder to notice):
print_int max 4 5 (* Error: This function has type int -> unit It is applied to too many arguments; maybe you forgot a `;'. *) print_int (max 4 5) (* Result: 5 *)
The following errors fall into one of the categories above but can be very hard to spot a beginner (like I was):
- Arithmetical operations, string or list concatenation must be in parentheses when used as an argument to a function :
print_int 2 + 3 (* Error: This expression has type unit but an expression was expected of type int *) print_int (2 + 3) (* Result: 5 *) print_string "a"^"b" (* Error: This expression has type unit but an expression was expected of type string *) print_string ("a"^"b") (* Result: ab *)
- Negative values also should use parentheses:
print_int -5 (* Error: This expression has type int -> unit but an expression was expected of type int *) print_int (-5) (* Result: -5 *)
- Sometimes the error points to a line that does not seem to be the cause of the problem. For instance in the following example I did not convert the argument of the
sqrtfunction to the
floattype but the error points to a totally different line:
let is_prime n = let rec is_prime_aux n div = if div > int_of_float (sqrt n) then true else if n mod div = 0 then false else is_prime_aux n (div + 1) in is_prime_aux n 2 (* Error: This expression has type float but an expression was expected of type int Points to `n` in `if n mod div...` *) let is_prime n = let rec is_prime_aux n div = if div > int_of_float(sqrt (float_of_int n)) then true else if n mod div = 0 then false else is_prime_aux n (div + 1) in is_prime_aux n 2
- Errors caused by partial application triggered by unintentionally passing fewer parameters that the function requires.
If you don’t want a biased opinion about OCaml you can stop reading now 🙂
Overall, I did not enjoy OCaml as a programming language. Initially, I struggled with errors that did not have much meaning to me. Once I got past this phase I found that I was not very productive – even conceptually simple problems required too much code for my taste. Maybe part of it was me learning the language but there were other people doing Advent of Code in OCaml and I found their solutions rarely required less code than mine. I also thought that it might be because I don’t use functional programming in my day-to-day work (except for quasi-functional features of C# like LINQ) and can’t switch to the functional paradigm but I briefly looked at my solutions in Scala from 2015 and they are generally much more compact.
Because I wanted to learn the language and not the libraries I wanted to get away as much as possible with just the basic language features (i.e. without using external libraries). It turned out to be hard. One of the most basic operation is writing a formatted string. OCaml offers a number of
print_* functions. Unfortunately, these functions are very basic and even writing a number followed by a new line requires two print statements. Printing a formatted string with these functions was so cumbersome that I eventually decided to use the
Another thing that baffled me at the very beginning was reading file contents. Most Advent of Code problems require reading the puzzle input from a file and I could not find any simple method to read lines from a file. It took me more time than I wanted to spend on this basic problem. All solutions seemed overly complicated. I eventually found this function on stackoverflow:
|let read_lines name : string list =|
|let ic = open_in name in|
|let try_read () =|
|try Some (input_line ic) with End_of_file -> None in|
|let rec loop acc = match try_read () with|
|| Some s -> loop (s :: acc)|
|| None -> close_in ic; List.rev acc in|
I have to admit that being a novice I initially did not fully understand how it worked but hey, it did work. The other alternatives I found were either even more complicated or required understanding concepts I did not event want to learn at the time (e.g.
channels). All in all, I have to say there is a heck of complexity to achieve a very basic task that in many languages is just a single, self-explanatory line of code – e.g.
I was also annoyed but by the lack of functions for simple string processing. To treat a string to as a sequence of characters you need to do something like this (again, I am not the author of this function):
|let explode s =|
|let rec exp i l =|
|if i < 0 then l else exp (i - 1) (s.[i] :: l) in|
|exp (String.length s - 1) |
In reality, it’s hard to do string processing without using the
Strmodule (btw. compare the name with the
Stringmodule that contains basic string operations and is part of the standard library and tell me how you would not be confused which one is which) which supports regular expressions.
Speaking of regular expressions I found them weird to use. The first weird thing was that you could not just get all matched groups (e.g. as a list). Rather, you need to enumerate them one by one without knowing how many groups where matched (so you potentially enumerate until you hit an error). The other weird thing was that it seems that matched groups (or maybe some state that allows calculating them) are stored in the library and are keyed by the original string that was processed. Each time you want to get a matched group you need to provide this original string.
API inconsistencies are also irritating. For instance, both
Hashtbl have the
fold_left function. However, while
List takes the function, the initial value and the list as parameters,
Hashtbl takes the function, the hash table and the initial value (i.e. the initial value and the container are swapped comparing to
List). It seems like a small thing, but I hit this many, many times.
Similarly, if you have multiple statements you need to separate them with a semicolon (
;). However, if you use multiple statements in the
else clause they also need to be wrapped with
begin/end (or parentheses). On the other hand, you don’t need
begin/end if you have multiple statements inside a loop (yes, I know, I should use recursion but sometimes using a loop is, you know, just simpler).
Runtime errors are a nightmare. You don’t get any details about the error except for the message. This might work if an exception is thrown from your own code but how you are supposed to find the bug effectively if all you get is:
Fatal error: exception Invalid_argument("index out of bounds")?
The last thing that I was surprised by was how scarce the documentation for OCaml is. When I decided to use OCaml to solve Advent of Code problems I knew it was not one of the mainstream programming languages, but I did not consider it completely niche. As soon as I started I realized that there is only a very limited number of resources at my disposal. Ironically, one of the best turned out to be the library reference on the https://caml.inria.fr website whose main page says:
This site is updated infrequently. For up-to-date information, please visit the new OCaml website at ocaml.org.
The library reference looks a bit raw and dated (e.g. see: https://caml.inria.fr/pub/docs/manual-ocaml/libref/String.html) and at the beginning I had a hard time digesting the information it provided but once I got more familiar with OCaml I would visit it all the time (yes, 50% of my visits were to check if the initial value for the
fold_left function should go before or after the container).
To sum up – the next time I start a new project and have freedom to choose the language for the project I don’t think OCaml will be on the list. I am glad I tried it but I hoped for a more pleasant ride.
Coincidentally, HackerRank released their 2018 Developer Skill Report recently and OCaml was one of only two languages with negative sentiment among developers of all age groups. I will just say that the other language was Perl.
P.S. My solutions to Advent of Code 2017 can be found on github.