There’s only one real pain point with OCaml, and that is that too few people know it. ![]()
8 posts were split to a new topic: Printf / ANSITerminal pain points
Sorry to jump in after the game, but I’d really like to respond to this point.
Personally (but this is just my opinion), I find it quite unpleasant to have to create several files (such as dune-project) in order to start getting a simple executable. I often find myself going back to ocamlfind and ocamlbuild in certain situations to quickly iterate through my work (which, in my opinion, is a failure of dune as it cannot handle such a use case).
I even sometimes regret certain possibilities offered by ocamlbuild (notably the ability to produce your own rules in great detail).
Finally, I never understood why we lost the {build} tag for dune in the description of opam dependencies. Again, from a release perspective, I think this was a big mistake.
That’s my two cents based on my experience. These points always make me reconsider dune as the solution. So I continue to try (much to the misfortune of some) other possibilities for building my software.
I find it quite unpleasant to have to create several files (such as
dune-project) in order to start getting a simple executable. I often find myself going back toocamlfindandocamlbuildin certain situations to quickly iterate through my work (which, in my opinion, is a failure ofduneas it cannot handle such a use case).
Maybe there is a lesson here from Make: if you call make in a directory without a Makefile, it will still be able to do useful things based on built-in rules. This can be used to quickly build a simple C project. I assume dune init was designed for this case but maybe we could have dune making some assumptions when it finds no dune-project but one or more ML files? That’s probably easier said than done because what to do in the case of several files is not obvious. Trying to locate packages in order to satisfy dependencies? Assuming we want to build an executable?
In the case of ocamlfind, you definitely need to express everything (like dependencies and files — in a topological order) but it more fast to write the appropriate command line than create a file. In the case of ocamlbuild, you need to explicitly ask what you want (like main.native) and it will try to solve things needed to build such artifact.
In both case, call ocamlfind or ocamlbuild alone (like dune build) do nothing as far as I can tell (specially for ocamlbuild).
Admittedly, this usage is rather “expert” (in the sense that it comes from my history of playing around with oasis to build an Eliom application!). Nevertheless, the frustration is great enough that I continue to use these tools every day.
Out of curiosity, what is the simplest way to compile and run this code?
let () =
let s = Unix.getenv "FOO" in
let s' = String.concat "&" Str.(split (regexp "and") s) in
Format.printf "%s" s'
I can think of several solutions (using utop, using ocamlfind, creating a dune project, using ocaml and #load "unix.cma", etc), but each of them seem to require memorizing some commands/flags which I always forget.
I think the quickest way is to run ocamlfind ocamlc unix.cma str.cma foo.ml -o foo and then ./foo, but there’s probably a better way.
If I were an OCaml newbie, I don’t know which approach I’d prefer for some “quick hacking”, i.e. the equivalent of writing this Python script:
import os
import re
s = os.getenv("FOO")
print("&".join(re.split(r"and", s)))
But this is a minor “pain point” that I often face when isolating code to try and reproduce bugs to be able to report them. I end up using dune init nowadays, but it creates several files, most of which I’ll discard anyway since I just want to compile a simple executable.
I don’t think that this will work with recent ocaml versions as upstream is gradually moving to the one library per directory informal convention. You should rather write:
ocamlfind ocamlc -linkpkg -package unix,str foo.ml
./a.out
My own answer to your question would be:
touch BRZO
brzo
or simply
brzo --root .
I’m making a mental note that 2026 should finally be the year where brzo gets released (it needs a final design round before, notably to replace the sexp based BRZO file by something nicer but note that it’s already usable if you pin the repo, though that sometimes also need a pin on the b0 package).
The defaults of dune init should get trimmed down, true. But compiling and running that program with dune is just two one-liner files and running dune exec ./foo.exe:
; dune-project
(lang dune 3.20)
; dune
(executable (name foo) (libraries unix str))
It’s not ideal and could be improved, but also not that much of a big deal I think?
Also, dune init project my-project will set up the my-project directory with a compilable dune project. Then all we need to do is add the unix and str libraries in the bin/dune file and the given source code in the bin/main.ml file, run dune build, and we are good to go.
No, it’s not bad actually, though there is less “locality” than the single-file solution:
- There are three needed files, instead of a single one;
- This can interfere with similar “scripts” in the same directory (in theory you can just add another
executablestanza, but other stanzas in a preexistingdunefile could affect it); - If a parent folder has a
dune-project, this may not work; - the output is put in
_build/default/foo.exe, which is a bit long to type and not very “local”.
Compared to the Python example where there is a single file, self-contained, and the presence of other Python files in the same directory is unlikely to impact it (except maybe if one of them is called os.py or re.py? I’m not sure how Python resolves imports in this case), or to a simple (fictitious example, does not work of course) gcc foo.c -lstr -lunix -o foo, it’s still a slightly more involved approach. It’s not necessarily worse, but requires a bit more “thinking”.
Indeed, the dune init project foo is the approach I use nowadays for such cases, but you still need to remember that libraries go in the executable stanza, the code is in bin, the output is in _build, and you can ignore directories lib and test. Also, consider adding --root . if a parent directory contains a dune-project (so I end up going to /tmp and doing such tests there, to avoid interference from the main project in which I’m working on).
Once you get used to it, it’s not bad, and definitely has its advantages, but there are a few surprising moments that happen now and then until you connect all the dots together.
The Python import is equivalent to a #load or #require in OCaml, excepted that #load/#require are only supported by toplevels (ocaml, utop). OCaml, looks more like Java. With Java, the javac compiler must be given the list of .jar files (the equivalent of .cma / .cmx). The Java import a.* is more like the OCaml open, a directive made to avoid too much typing, but doesn’t make a class available by itself. (And import a.b should be spelt module B = A.B in OCaml. Also a way to reduce typing).
The main difference is that in Python, when we install a library, nothing is packaged in an archive. All packages or modules are directly accessible as a file with the same name. With Java and OCaml, anything is packaged in a .jar or .cma/.cmx, then we have to specify this file.
With Java, dedicaded IDEs present a folder where we can drag-and-drop the needed .jar, the developper can easily add them.
(The Python import can also serve as a Java import, with the form import x.x.x as x).
The whole point of findlib is that you don’t have to specify the archive names for your dependencies!
ocamlfind ocamlc -package unix,str -linkpkg foo.ml -o foo
(that also eliminates the warning you’d see with OCaml 5.x, as @dbuenzli notes)
Makefile is a necessary tool to learn if one uses Linux daily.
All my OCaml projects have a makefile.
It has greatly aided my workflow.
Well said.
In my opinion dune is still way better than gradle to me.
Gradle was unbelievably complex to me.