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.
Common errors
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.
Syntax error
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:
- missing
in
after variable declaration:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterslet 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
=>
instead of->
(likely specific to developers who mostly use C# or JavaScript/TypeScript) - using reserved words as variable names (I fell a few times for
val
andmatch
I wanted to use respectively for a variable storing a temporary value and a result of regular expression match) - missing
->
in pattern matching (the exact error isSyntax error: pattern expected
Unbound value
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
rec
:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterslet 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 `;'.
Similarly to Syntax error
there can be multiple causes for this error:
- You actually forgot a
;
to separate your statements:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersprint_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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterslet 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):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersprint_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 *)
WTF errors
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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersprint_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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersprint_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
sqrt
function to thefloat
type but the error points to a totally different line:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characterslet 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.
Observations
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 Printf
module.
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 | |
loop [] |
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. File.ReadAllLines("myFile.txt")
.
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
Str
module (btw. compare the name with the String
module 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 List
and 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 then
or 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.
Hi,
thanks for the report.
I had quite a similar experience when learning Ocaml. Once I got to a certain level without the beginner noisy work, I really enjoyed the quietness and power of this language.
Here are af few comments:
– the OCaml refman offers all the required information ; it looked old-fashioned, but it has changes and it’s the place to go
– you can and should/must build your higher level functions with the advantage that you know how they are build (however, Core and Batteries should offer much more functions than the stdlib that is intentionally kept slim)
– you shall not use loops because you do not need them at all if you think function and recursion over your data structure;
– imperative features of the language are fine when dealing with libraries such as GTK or similar (it’s a way to use sequences, loops, ref…)
– one good way for learning OCaml (or any other language) is to play with OCaml code chunks using stdlib and libraries in the toplevel, in an interactive mode. It gives you instant feedback and lets you learn and build a program in a pleasant way; it’s much more efficient than programming/compiling/linking/executing (one of your modes).
BTW, within vscode/vscode-reasonml, do you have an integrated toplevel and a debugger?
I just know that you can program your build command.
Thanks
LikeLike
Hi Roger,
Thanks for your comments. It’s been almost two years and I have not had a chance/need to use OCaml since then. I am sure that a lot has changed in the OCaml world since my exercise but I have not been following. I was also using the language in a very specific context of solving the Advent of Code problems which allows to get a feel of the language but typically does not require more advanced features/concepts of the language.
– I have mostly used google/bing to get help and at that time the search results were lacking
– agreed, however the Advent of Code problems rarely require a lot of code (most of solutions are less than 100 lines total) so the overall complexity is low and this is not a problem
– I agree in principle but there were cases where it was much easier/shorter to just use a loop (or I was frustrated enough with both the problem and the language that I did not have the energy to convert the loop if it just worked)
– Advent of Code problems don’t require using libraries (some of the problems are actually faster solved with pen and paper than by writing code)
– I did use REPL to test syntax etc.
IIRC at that time reasonml only supported syntax coloring and simple parsing. I remember compiling from the command line and I am sure debugging was not supported.
Thanks
LikeLike