Skip to main content

Navigating OCaml Projects

Last Updated: 31, July 2020 at 03:05:12

Platform tools: Utop

Overview

Looking at the root of an OCaml project can be intimidating - there can be many files and many directories. There are, however, some common standards and best practices that many OCaml libraries use. This workflow aims to help you navigate most OCaml projects.

Structure

Files in the top-level directory

What follows are brief descriptions of files you will likely see in the top-level directory of the project (usually the first page you see when you open a project on Github) and what they do.

  • Makefile - make is a build tool and the Makefile tells make what to do. Quite a few OCaml projects use a Makefile to wrap dune build commands, especially those with non-obvious arguments. Unless your project is complex you don't need a Makefile but it is useful to know why you might come across one.
  • .gitignore - a file that tells git which files or folders it should ignore. Some standard OCaml things you might consider ignoring are: dune's build directory (_build), local opam switches (_opam) and merlin files (.merlin).
  • .ocamlformat and .ocp-indent - these are configuration files for the OCamlFormat and Ocp-indent tools respectively. It will have, at a bare minimum, a version number inside. For more information on setting this up see the relevant workflow.
  • dune-project - a file which marks the root of the project. It will usually be generated for you whenever you run dune build for the first time. It contains a lang stanza which tells dune what version you are using. It is also recommended that you generate your opam files from here.
  • *.opam or opam - these are opam files which contain project metadata and dependency information.
  • *.opam.template - not everything can be generated by your dune-project file for the opam file, the template file lets you add additional data like pin-depends for adding unreleased versions of packages.
  • .travis.yml and appveyor.ml - these are continuous integration configuration files for TravisCI and AppVeyor respectively.

Directory Layout

How directories are laid-out for a project varies greatly depending on what is being built and who is building it. What follows is an attempt at describing the most general and common project structure. Our running example will be a library called explore that also makes a command-line tool.

Here is what a typical structure may look like.

.
|-- bin
|   |-- cli.ml
|   `-- dune
|-- dune-project
|-- explore-unix.opam
|-- explore.opam
|-- lib
|   |-- explore
|   |   |-- dune
|   |   |-- explore.ml
|   |   |-- explore.mli
|   |   |-- files.ml
|   |   `-- files.mli
|   `-- explore-unix
|       |-- dune
|       |-- explore_unix.ml
|       `-- explore_unix.mli
`-- test
    |-- test.ml
    |-- test_files.ml
    `-- test_files.mli

There are three main directories:

  1. bin - short for binaries, this is where the executables live. The dune file will likely be building an executable.
  2. lib - short for libraries, this is where code that other programs can use lives. The dune file will likely be building a library.
  3. test - the testing of the project is done here. The dune file will likely be building a test suite. This can be a good place to see the project being used in practice if you are unsure how to use it.

You may have noticed there are two opam files, this is actually quite common. Sometimes you want highly dependent code to live in the same respository together like the main explore library and the unix implementation, explore-unix. Or maybe your library also has a ppx that can be a separate opam package.

The Omnipresent Dune File

Nearly every directory contains a dune file. Dune uses these to understand the structure of your project and build it appropriately. Running dune runtest for example knows to go looking in the test folder because the dune file in it has a test stanza. For more on this, check out the dune page.

Implementation and Interfaces

OCaml is built around the idea of compilation units. Each unit being made up of an implementation (the .ml file) and an interface or specification (the .mli file). The interface is like the contractual obligation for what the implementation must provide.

This means the interface files are a good place to start looking if you are using a library or want to get a broad overview of how a project is structured. It answers questions like which types are abstracted (i.e. appear as just type t in the file) and what submodules are defined for you to use. This information helps in knowing what you can and can't do.

It is also where most documentation is written and as you become more familiar with reading type information, it can be a very useful way to properly use someone's library.

Utop offers good support for quickly inspecting other code, as does the VS Code platform.

Miscellaneous Files and Directories

There are many different ways to structure an OCaml project, particularly when you are using Dune. What follows are a few more files and directories to look out for as you explore other OCaml projects.

  • fuzz and bench directories - these hold fuzz tests and benchmarks respectively.
  • doc directory or gh-pages branch - either one of these may contain the result of producing documentation with the odoc tool. The documentation workflow explains how this works.

Resources

  1. The _intf trick - Craig Ferguson explains a different way of laying out OCaml compilation units to reduce duplication of type information and the tradeoffs associated with it. This is a technique commonly used by Jane Street in their OSS.

Edit this page on Github