Quickstart an OCaml app project using Dune
Updated June 2022
Once you’re done playing with OCaml snippets online, you’ll want to build an actual OCaml project. Let’s go through the process of doing so.
To be able to follow this tutorial, you need to have
opam, the OCaml package manager, installed.
Follow the basic instructions here to install
You can verify your opam installation with the following command:
$ opam --version 2.1.2
Your version of
opam may be greater, and that should be fine.
This tutorial generally assumes a MacOS or Linux environment.
For Windows, you can use
WSL or see Windows Support for more information.
See Editor Setup for tips on setting up your environment to work with OCaml.
Initial dune project
dune is a sophisticated tool. We can use it to create our project for us:
dune init proj project_name
project_name is whatever you want to call your project.
Dune will create a directory named
project_name, containing the following subdirectories and files:
bin/ (the place for executable code) bin/dune (a basic dune file specifying how to build the main executable) bin/main.ml (our main executable code) lib/ (the place for library code) lib/dune (a basic dune file specifying how to build the library) test/ (the place for test code) test/project_name.ml (a dummy test file) test/dune (a dune file specifying how to build our tests) dune-project (a configuration file for the entire project) project_name.opam (an opam file that's created automatically by dune)
Let’s try to get
dune to build the dummy code it wrote for us:
$ dune build
dune will now build the dummy code.
dune has now added the
This directory contains all of the artifacts which were used to build our project.
We can now use
dune to run our code:
$ dune exec ./bin/main.exe Hello, World!
The dummy code in
./bin/main.ml writes ‘Hello World!’ to the screen,
and when we execute it we see the result.
Every directory where we compile something needs a
dune file format uses
sexp; much like the languages
It’s a little counter-intuitive, but not too complicated once you get used to it.
Let’s take a look at the file
./bin/dune, which compiles our main application:
(executable (public_name project_name) (name main) (libraries project_name))
The parentheses may make things look confusing, but consider that every parenthesis pair indicates a sentence or command.
First we have the
This indicates that within the parenteses, we’ll have information about building the executable.
public_name is the command indicating the external name of the project.
name indicates the name of the main file of the executable.
libraries introduces a list of the libraries we’ll be using in this project,
which in this case is just the dummy library
dune created for us under
How do we add more stuff for
dune to compile?
Most of the time, we’ll just need to list the libraries we need to use under
This can include other OCaml libraries from
Now let’s take a look at the
dune created for us:
(lang dune 3.2) (name project_name) ; The name of our project (generate_opam_files true) ; Always keep this true (source (github username/reponame)) ; Once you have a github repo, place it here (authors "Author Name") ; Your name (maintainers "Maintainer Name") (license LICENSE) (documentation https://url/to/documentation) (package (name project_name) (synopsis "A short synopsis") (description "A longer description") (depends ocaml dune) ; Add your opam dependencies here (tags (topics "to describe" your project))) ; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project
Here we see a lot of fields to fill in once we know more about our project.
For a play project, we can leave these fields as they are,
but if we want to get serious about our project, we want to fill them in.
Most importantly, we want to make sure we fill in the
depends field to have all the
opam libraries we’re using.
dune build after updating our
dune_project file will transfer this information to the
project_name.opam file so
opam knows how to handle our project.
Do not update
project_name.opam manually! Let
dune do it for you.
We’re now going to invoke
dune manages building your individual project,
opam (the OCaml Package Manager) handles downloading all the other necessary OCaml packages.
dune speaks in terms of OCaml executables and libraries – the products of OCaml
opam speaks in terms of OCaml packages – other packaged bits of OCaml code that depend on each other.
Fortunately, we don’t need to write any complex configuration file for
dune handles it for us!
Next up, we are going to create an
opam switch. Opam install packages globally, and switches makes it easy
to have a isolated environment in which you can install packages that will not be shared between switches,
and therefore reduce the chance of dependencies conflict. You can think of it as an analog to Python’s
opam switch contains a whole bunch of installed packages that are specified as compatible with each other.
However, it can only have one installed version of each package!
We’re going to create an entirely new
opam switch at our main directory.
This switch will include an OCaml compiler, our project dependencies, as well as the project itself.
We do all of this by making sure we’re in our main directory and typing
opam switch create .
opam will respond with
Package project_main does not exist, create as a NEW package? [Y/n]
Answer with yes (y) to create a new local package for our project.
opam will now pin our project and install a whole bunch of stuff needed for our dependencies.
opam tells us to
Run eval $(opam env) to update the current shell environment
Do as it asks by running
eval $(opam env):
We need to update our shell to know about the current
Nowadays it’s become quite popular to ‘lock’ projects to specific dependency versions. This allows us to recreate the exact same environment on other installations. There are two ways to restrict versions:
We can specify the exact versions of our dependencies in the
depends. For example, we could specify
if we want to specify a minimal version.
Alternatively, we can have
opamautomatically specify the exact versions of our dependencies using the command
opam lock .
opamwill create a new file for us,
project_name.opam.locked, which will have these exact dependencies listed.
At this point we have a bunch of files and directories.
What do we include in git?
Obviously, we want to place the
test directories and their contents in git versioning.
We also need to include the
dune_project and opam files.
In fact, the only things we don’t want to include are the
It’s a good idea to add a
.gitignore file containing
and then to add that to our git repository as well.
Expanding our dummy application
dune created a nice little skeleton application for us.
All we have to do is expand it!
- If we’re writing a regular application, use
./bin/main.mlas the starting point. Add subdirectories and additional
.mlfiles as needed.
Structuring your code in a modular way is one of software engineering best practices. In OCaml and Dune, such structure can be achieved through the use of libraries. While there may be a formal definition of it, I like to think of a library as a collection of modules that can be depended on.
In this section, I am going to show you how to create a library with Dune. In many open source OCaml libraries I encounter, it would seem that a flat directory structure is preferred, so we are going with that in this tutorial. Note that while this might be okay in most cases, you might want to create multiple libraries if your codebase is “big enough”.
So let’s get to it! Let’s create another dune file inside the lib directory as follows:
(library (name lib))
Observe that instead of executable, we now use the library stanza. It also has the field name just like an executable. The value you put in the name field will be the identifier that you can use to refer to this library from other executables or libraries.
To illustrate the use of libraries, we are going to add a Math module to this library, which exposes two functions, add and sub. With your editor, create a math.ml file inside the lib directory:
let add x y = x + y let sub x y = x - y
Nothing too hard here, we only defined the two functions. Now, let’s make our main executable depends on this library. Open bin/dune file on your editor and add lib (the name of your library) to the libraries field of your executable:
(executable (name main) (libraries lib))
Great! Then, let’s first rebuild the project so our editor can pick up and resolve the new dependency that we just add:
$ dune build bin/main.exe
(The above step is not really necessary, but if we don’t do it we will see errors on our editor if we try to refer any modules and functions from the new library we just add until we rebuild the project.)
Now, open up
bin/main.ml and replace the contents with the following:
open Lib let () = let result = Math.add 2 3 in print_endline (string_of_int result); let result = Math.sub 3 1 in print_endline (string_of_int result)
At the first line, we open Lib so that all the modules under it (currently only Math) is available in scope. We then use functions from the new Math module and print out the results.
Let’s try it:
$ dune exec bin/main.exe 5 2
If I remember my elementary school math subject correctly, our program now prints the correct result of the functions from our library!
One thing you may (or may not) observe from the above steps is that anything you write on math.ml module will be automatically visible from the client module (in this case, main.ml). This is how OCaml works; by default all identifiers are exposed from a module.
Sometimes — well, most of the times, in fact — this is not what you want. You may have several internal small helper functions to do your job, but you don’t want other modules to use those functions directly. There is a way to do that, which is by using interface files.
.ml files contain implementations, interfaces are put into files with .mli extensions.
An interface file is also the place where developers put API doc comments.
Suppose that for some reason, we design our Math module to only expose add function, and leave sub unexposed for now. We can do it by defining the following math.mli file inside lib directory:
val add : int -> int -> int (** [add x y] returns the result of x + y. *)
In this instance, you might consider the comment redundant, but I am a fan of always putting documentations for public facing APIs It gives me that sense of safety: “hey, it’s documented, so it should be safe to use.”
Now, what happens if you leave main.ml as it is and you try to build the project?
$ dune exec bin/main.exe (...some output...) File "bin/main.ml", line 6, characters 15-23: Error: Unbound value Math.sub (...some output...) File "lib/math.ml", line 3, characters 4-7: Error (warning 32): unused value sub.
Whoops, the build fails because Math.sub is no longer found! That is expected, because with our interface as it is we only allow add to be visible to the client modules. You can see how we can limit the public interface of our modules with this technique, and keep internal implementation details, um, internal.
Okay, so how could we change the interface file to solve this build failure? You would add a declaration of sub! Here’s what you might end up with:
val add : int -> int -> int (** [add x y] returns the result of x + y. *) val sub : int -> int -> int (** [sub x y] returns the result of x - y. *)
You can try to build and run it with
dune exec like we did before,
and it will run correctly.
Trying out libraries interactively via utop
Another benefit of having the libraries in a separate directory from the executables is that you can use utop REPL to play around with your functions. The REPL will evaluate the files given, so if any of it produces a side-effect on evaluation time (e.g. printing, starting a web server) like what typically executables entry point do, it will be run on starting the REPL, which may not be what you want.
Let’s have a look on how to use the REPL.
With Dune and utop installed, let’s invoke
dune utop lib.
dune utop <dir> is a convenient way to invoke utop while having the
<dir> (in this case, lib) automatically built and loaded.
Let’s try out our library (I will only be showing the prompt and result, and not the extra graphics that you might actually see):
utop # open Lib;; utop # #show Math.add;; (* 1 *) val add : int -> int -> int utop # Math.add 1 2;; (* 2 *) - : int = 3 utop # let add2 = Math.add 2;; (* 3 *) val add2 : int -> int = <fun> utop # add2 5;; (* 4 *) - : int = 7
Note the double semicolon! It’s necessary to tell utop that we want to evaluate the expression.
In this snippet, we tried opening the Lib, and:
- Querying a type of a function;
- Actually invoking a function;
- Partially applying a function; and
- Invoking the partially-applied function with the remaining arguments.
I personally find utop to be a helpful tool when I want to interactively try the functions I defined. It can give fast feedback on the behavior and the API of the modules, so that I can quickly tweak things if I find something to be unsatisfactory. If I made changes to the code and want to reload it in utop, I usually just exit the session and spawn another one, since it’s quite fast.
To exit the utop session, you can use
CTRL+D or use
That concludes our exploration on Dune to build our OCaml app project. I have by no means exhausted the capabilities that Dune have, but what I demonstrated here is sufficient to get you up and running with your own app.