OCaml Best Practices

Workflows presented on this page assume OCaml is Up and Running.

Bootstrap a project

Dune is recommended for bootstrapping projects using dune init. If opam or dune are not installed, please see Up and Running with OCaml.

dune init --help

dune init {library,executable,test,project} NAME [PATH] initialize a new dune component of the specified kind, named NAME, with fields determined by the supplied options.

As shown above, dune init accepts a kind, NAME, and optional PATH to scaffold new code. Let's try it out:

dune init project hello ~/src/ocaml-projects

Success: initialized project component named hello

In the above example, we use:

  • "project" as the kind
  • "hello" as the name, and
  • "~/src/ocaml-projects" as the path to generate the content in

The project kind creates a library in ./lib, an executable in ./bin, and links them together in bin/dune. Additionally, the command creates a test executable and an opam file.

tree ~/src/ocaml-projects/hello/

/home/user/src/ocaml-projects/hello/
├── bin
│   ├── dune
│   └── main.ml
├── hello.opam
├── lib
│   └── dune
└── test
    ├── dune
    └── hello.ml

At this point, you can build the project and run the binary:

cd /home/user/src/ocaml-projects/hello/
dune exec bin/main.exe

Hello, world!

Thus, dune init can rapidly scaffold new projects, with minimal content. It can also be used to add components (kinds) incrementally to existing projects.

Various community projects offer more comprehensive project scaffolding than dune as well. The following projects are not formally supported by the OCaml Platform, but may be of interest to the reader:

Installing dependencies

TL;DR

opam switch create . --deps-only --with-test --with-doc

It is recommended to install the dependencies of a project in a local opam switch to sandbox your development environment.

If you're using opam 2.0.X, you can do this with:

# if you need external dependencies
opam pin add -n .
opam depext -i <packages>
opam install . --deps-only --with-test --with-doc

If you use opam 2.1.X, it will install the system dependencies automatically, so you can run:

opam install . --deps-only --with-test --with-doc

Now, if for some reason you prefer to install your dependencies in a global switch, you can run:

opam switch set <switch_name>
opam install . --deps-only --with-test --with-doc

Once the dependencies have been installed successfully, and assuming the project uses dune as the build system, you can compile it with:

opam exec -- dune build

Or if you set your environment with eval $(opam env):

dune build
Updating dependencies

TL;DR

If the project generates the *.opam file from the dune-project, add the dependency in the package stanza and run opam install . --deps-only. If the project does not generate the *.opam file, add the dependency in the *.opam file and run opam install . --deps-only. To avoid duplicating the project configuration into multiple files, Dune allows to generate the *.opam file of the project from the package definitions in dune-project when adding the (generate_opam_files true) stanza.

However, opam remains a central piece of the ecosystem and it's very likely that you will have to work with *.opam files at some point, so we don't take a stance on wether you should specify your dependencies in the *.opam file or in dune-project.

If the project generates the opam file from the dune-project (you can tell by the line # This file is generated by dune, edit dune-project instead at the top of the *.opam file), you can add your dependencies in the dune-project in the appropriate package stanza. It should look like this:

(package
 (name demo)
 (synopsis "A short, but powerful statement about your project")
 (description "An complete and exhaustive description everything your project does.")
 (depends
  (ocaml
   (>= 4.08.0))
  dune
  (alcotest :with-test)
  (odoc :with-doc)))

Once you have added your dependency, you can build your project with dune build which will re-generate the *.opam files.

If the *.opam files are not generated, you can add the dependencies in them directly, in the depends field. If should look like this:

opam-version: "2.0"
synopsis: "A short, but powerful statement about your project"
description: "An complete and exhaustive description everything your project does."
depends: [
  "ocaml" {>= "4.08.0"}
  "dune"
  "alcotest" {with-test}
  "odoc" {with-doc}
]
build: [
  ["dune" "subst"] {pinned}
  [
    "dune"
    "build"
    "-p"
    name
    "-j"
    jobs
    "@install"
    "@runtest" {with-test}
    "@doc" {with-doc}
  ]
]

Either way, once you have added your dependency in the appropriate file, you can run opam install . --deps-only to update your current switch dependencies.

Updating development dependencies

TL;DR

Follow the workflow "Update dependencies" and add a flag :with-test or with-doc to your dependency. Opam does not have a notion of development dependencies. Instead, each dependency can be either:

  • A normal dependency (used at runtime)
  • A build dependency (used to build the project)
  • A test dependency (used to test the project)
  • A documentation dependency (used to generate the documentation)

When adding a new dependency, as seen in the "Update dependencies" workflow, you can add a flag to your dependency.

For dune-project, it looks like this:

(alcotest :with-test)

And for the *.opam file, it looks like:

"alcotest" {with-test}

The available flags for each dependencies are:

  • Normal: no flag
  • Build: build
  • Test: with-test
  • Documentation: with-doc

See opam documentation for more details on the opam syntax.

Selecting a compiler

TL;DR

Use opam switch set to manually select the switch to use and use dune-workspace to automatically run commands in different environment.

Compilation environments are managed with opam switches. The typical workflow is to have a local opam switch for the project, but you may need to select a different compilation environment (i.e. a different compiler version) sometimes. For instance, to run unit tests on an older/newer version of OCaml.

To do this, you'll need to create global opam switches. To create an opam switch with a given version of the compiler, you can use:

opam switch create 4.14.0 ocaml-base-compiler.4.14.0

This will create a new switch called 4.14.0 with the compiler version 4.14.0.

The list of available compiler version can be retrieved with:

opam switch list-available

This will list the available compiler version for all of the configured Opam repositories.

Once you've created a switch (or you already have a switch you'd like to use), you can run:

opam switch set <switch_name>
eval $(opam env)

to configure the current environment with this switch.

If it is a new switch, you will need to reinstall your dependencies (see "Installing dependencies") with opam install . --deps-only.

Alternatively, you may want to automatically run commands in a given set of compilation environments. To do this, you can create a file dune-workspace at the root of your project and list the opam switches you'd like to use there:

(lang dune 2.0)
(context (opam (switch 4.11.0)))
(context (opam (switch 4.12.0)))
(context (opam (switch 4.13.0)))

All the Dune commands you will run, will be run on all of the switches listed. For instance with the definition above:

dune runtest --workspace dune-workspace

Dune will run the tests for OCaml 4.11.0, 4.12.0 and 4.13.0.

Running executables

TL;DR

Add an executable stanza in your dune file and run the executable with dune exec <executable_path>.exe or dune exec <public_name>.

To tell dune to produce an executable, you can use the executable stanza:

(executable
 (name <executable_name>)
 (public_name <public_name>)
 (libraries <libraries...>))

The <executable_name> is the name of the executable used internally in the project. The <public_name> is the name of the installed binary when installing the package. Finally, <libraries...> is the list of libraries to link to the executable.

Once dune has produced the executable with dune build, you can execute it with dune exec <executable_path>.exe or dune exec <public_name>.

For instance, if you've put your dune file in bin/dune with the following content:

(executable
 (name main)
 (public_name my-app)
 (libraries))

You can run it with dune exec bin/main.exe or dune exec my-app.

Running tests

TL;DR

Add a test stanza in your dune file and run the tests with dune build @runtest.

Tests are created using Dune's test stanza. The test stanza is a simple convenience wrapper that will create an executable and add it to the list of tests of the @runtest target.

For instance, if you add a test in your dune file:

(test
 (name dummy_test)
 (modules dummy_test))

with a module dummy_test.ml:

let () = exit 1

Running dune build @runtest will fail with the following output:

  dummy_test alias src/ocamlorg_web/test/runtest (exit 1)

This means that the test failed because the executable exited with the status code 1.

The output is not very descriptive. If we want to create suites of unit tests, with several tests per files, and different kind of assertions, we will want to use a test framework such as Alcotest.

Let's modify our dummy test to link to Alcotest:

(test
 (name dummy_test)
 (modules dummy_test)
 (libraries alcotest))

With the following module:

open Alcotest

let test_hello_with_name name () =
  let greeting = "Hello " ^ name ^ "!" in
  let expected = Printf.sprintf "Hello %s!" name in
  check string "same string" greeting expected

let suite =
  [ "can greet Tom", `Quick, test_hello_with_name "Tom"
  ; "can greet John", `Quick, test_hello_with_name "John"
  ]

let () =
  Alcotest.run "Dummy" [ "Greeting", suite ]

If we run dune build @runtest again, the test should be successful and output the following:

Testing `Dummy'.
This run has ID `B5926D16-0DD4-4C97-8C7A-5AFE1F5DF31B'.

  [OK]          Greeting          0   can greet Tom.
  [OK]          Greeting          1   can greet John.

Full test results in `_build/default/_build/_tests/Dummy'.
Test Successful in 0.000s. 2 tests run.
Creating libraries

TL;DR

Add a library stanza in your dune file.

Creating a library with dune is as simple as adding a library stanza in your dune file:

(library
 (name <name>)
 (public_name <public_name>)
 (libraries <libraries...>))

Where <name> is the name of the library used inside internally, <public_name> is the name of the library used by users of the package and <libaries...> is the list of libraries to link to your library.

Note that if the library does not have a public_name, it will not be installed when installing the package through opam. As a consequence, you cannot use an internal library that does not have a public_name in a library or executable that has one.

Publishing packages

TL;DR

Create a CHANGES.md file and run dune-release bistro.

The opam package manager may differ from the package manager you're used to. In order to ensure the highest stability of the ecosystem, each package publication goes through two processes:

  • An automated CI pipeline which tests if your package installs using multiple distributions and multiple OCaml compiler versions. It will also check that your new release does not break your reverse dependencies (those packages that require your package). A lower-bound check also ensures that your package installs with the lowest version of your package's dependencies.
  • A manual review of the package metadata by an opam-repository maintainer.

This process starts with a PR to the opam-repository, with the addition of a file for the version of the package to publish. The file contains information such as the package name, description, VCS repository, and most importantly, the URL the sources can be downloaded from.

If everything looks good and the CI build passes, the PR is merged and the package becomes available in opam after an opam update to update the opam-repository.

If there is anything to change, an opam-repository maintainer will comment on the PR with some recommendations.

This is a heavy process, but hopefully, all of it is completely automated on the user side. The recommended way to publish a package is dune-release.

Once you're ready to publish your package on opam, simply create a CHANGES.md file with the following format:

# <version>

<release note>

# <older version>

<release note>

and run dune-release bistro.

Dune Release will run some verification (such as running the tests, linting the opam file, etc.) and will open a PR for you on opam-repository. From there, all you have to do is wait for the PR to be merged, or for a maintainer to review your package publication.

Setting up VSCode

TL;DR

Install the VSCode extension ocamllabs.ocaml-platform and the packages ocaml-lsp-server ocamlformat in your opam switch.

The official OCaml extension for VSCode is https://marketplace.visualstudio.com/items?itemName=ocamllabs.ocaml-platform.

To get started, you can install it with the following command:

ext install ocamllabs.ocaml-platform

The extension depends on OCaml LSP and ocamlformat. To install them in your switch, you can run:

opam install ocaml-lsp-server ocamlformat

When running vscode from the terminal, the extension should pick up your current opam switch. If you need to change it, you can click on the package icon in the status bar to select your switch.