Libraries With Dune
Introduction
Dune provides several means to arrange modules into libraries. We look at Dune's mechanisms for structuring projects with libraries that contain modules.
This tutorial uses the Dune build tool. Make sure you have version 3.7 or later installed.
In the following toy project, we create an OCaml library for the World Meteorological Organization, with clouds as its subject.
We use unique terms for different elements of our project to avoid ambiguity. For instance, the directory containing the file cloud.ml isn't named cloud. A different term is used. We also use the Spanish word "nube" and the Nahuatl word "mixtli," where both terms mean "cloud."
Note: The other terms we use are classifications of clouds. These include "cumulus" (fluffy), "nimbus" (precipitating), "cumulonimbus" (fluffy and precipitating), "nimbostratus" (flat, amorphous and precipitating), and "altocumulus" (high altitude and fluffy).
Minimum Project Setup
This section details the structure of an almost-minimum Dune project setup. Check Your First OCaml Program for automatic setup using the dune init proj command.
$ mkdir mixtli; cd mixtli
In this directory, create four more files: dune-project, dune, cloud.ml, and wmo.ml:
dune-project
(lang dune 3.7)
(package (name wmo-clouds))
This file contains the global project configuration. It's kept almost to the minimum, including the lang dune stanza that specifies the required Dune version and the package stanza that makes this tutorial simpler.
dune
(executable
(name cloud)
(public_name nube))
Each directory that requires some sort of build must contain a dune file. The executable stanza means an executable program is built.
- The
name cloudstanza means the filecloud.mlcontains the executable. - The
public_name nubestanza means the executable is made available using the namenube.
wmo.ml
module Stratus = struct
let nimbus = "Nimbostratus (Ns)"
end
module Cumulus = struct
let nimbus = "Cumulonimbus (Cb)"
end
cloud.ml
let () =
Wmo.Stratus.nimbus |> print_endline;
Wmo.Cumulus.nimbus |> print_endline
Here is the resulting output:
$ opam exec -- dune exec nube
Nimbostratus ()
Cumulonimbus ()
Here is the directory contents:
$ tree
.
├── dune
├── dune-project
├── cloud.ml
└── wmo.ml
Dune stores the files it creates, and a copy of the sources, in a directory named _build. You don't need to edit anything there. In a project managed using Git, the _build directory should be ignored
$ echo _build >> .gitignore
You can also configure your editor or IDE to ignore it too.
In OCaml, each .ml file defines a module. In the mixtli project, the file cloud.ml defines the Cloud module, the file wmo.ml defines the Wmo module that contains two submodules: Stratus and Cumulus.
Here are the different names:
mixtliis the project's name (it means cloud in Nahuatlcloud.mlis the OCaml source file's name, referred ascloudin thedunefile.nubeis the executable command's name (it means cloud in Spanish).Cloudis the name of the module associated with the filecloud.ml.Wmois the name of the module associated with the filewmo.ml.wmo-cloudsis the name of the package built by this project.
The dune describe command allows having a look at the project's module structure. Here is its output:
((root /home/cuihtlauac/caml/mixtli-dune)
(build_context _build/default)
(executables
((names (cloud))
(requires ())
(modules
(((name Wmo)
(impl (_build/default/wmo.ml))
(intf ())
(cmt (_build/default/.cloud.eobjs/byte/wmo.cmt))
(cmti ()))
((name Cloud)
(impl (_build/default/cloud.ml))
(intf ())
(cmt (_build/default/.cloud.eobjs/byte/cloud.cmt))
(cmti ()))))
(include_dirs (_build/default/.cloud.eobjs/byte)))))
Libraries
In OCaml, a library is a collection of modules. By default, when Dune builds a library, it wraps the bundled modules into a module. This allows having several modules with the same name, inside different libraries, in the same project. That feature is known as namespaces for module names. This is similar to what modules do for definitions; they avoid name clashes.
Dune creates libraries from directories. Let's look at an example. Here the lib directory contains its sources. This is different from the Unix standard, where lib stores compiled library binaries.
$ mkdir lib
The lib directory is populated with the following source files:
lib/dune
(library (name wmo))
lib/cumulus.mli
val nimbus : string
lib/cumulus.ml
let nimbus = "Cumulonimbus (Cb)"
lib/stratus.mli
val nimbus : string
lib/stratus.ml
let nimbus = "Nimbostratus (Ns)"
All the modules found in the lib directory are bundled into the Wmo module. This module is the same as what we had in the wmo.ml file. To avoid redundancy, we delete it:
$ rm wmo.ml
We update the dune file building the executable to use the library as a dependency.
dune
(executable
(name cloud)
(public_name nube)
(libraries wmo))
Observations:
- Dune creates a module
Wmofrom the contents of directorylib. - The directory's name (here
lib) is irrelevant. - The library name appears uncapitalised (
wmo) indunefiles:- In its definition, in
lib/dune - When used as a dependency in
dune
- In its definition, in
Library Wrapper Modules
By default, when Dune bundles modules into a library, they are automatically wrapped into a module. It is possible to manually write the wrapper file. The wrapper file must have the same name as the library.
Here, we are creating a wrapper file for the wmo library from the previous section.
lib/wmo.ml
module Cumulus = Cumulus
module Stratus = Stratus
Here is how to make sense of these module definitions:
- On the left-hand side,
module Cumulusmeans moduleWmocontains a submodule namedCumulus. - On the right-hand side,
Cumulusrefers to the module defined in the filelib/cumulus.ml.
Run dune exec nube to see that the behaviour of the program is the same as in the previous section.
When a library directory contains a wrapper module (here wmo.ml), it is the only one exposed. All other file-based modules from that directory that do not appear in the wrapper module are private.
Using a wrapper file makes several things possible:
- Have different public and internal names,
module CumulusCloud = Cumulus - Define values in the wrapper module,
let ... = - Expose module resulting from functor application,
module StringSet = Set.Make(String) - Apply the same interface type to several modules without duplicating files
- Hide modules by not listing them
Include Subdirectories
By default, Dune builds a library from the modules found in the same directory as the dune file, but it doesn't look into subdirectories. It is possible to change this behaviour.
In this example, we create subdirectories and move files there.
$ mkdir lib/cumulus lib/stratus
$ mv lib/cumulus.ml lib/cumulus/m.ml
$ mv lib/cumulus.mli lib/cumulus/m.mli
$ mv lib/stratus.ml lib/stratus/m.ml
$ mv lib/stratus.mli lib/stratus/m.mli
Change from the default behaviour with the include_subdirs stanza.
lib/dune
(include_subdirs qualified)
(library (name wmo))
Update the library wrapper to expose the modules created from the subdirectories.
wmo.ml
module Cumulus = Cumulus.M
module Stratus = Stratus.M
Run dune exec nube to see that the behaviour of the program is the same as in the two previous sections.
The include_subdirs qualified stanza works recursively, except on subdirectories containing a dune file. See the Dune documentation for more on this topic.
Remove Duplicated Interfaces
In the previous stages, interfaces were duplicated. In the
“Libraries” section of this tutorial, two files are the same:
lib/cumulus.mli and lib/status.mli. Later, in the “Include
Subdirectories” section, the files lib/cumulus/m.mli
and lib/status/m.mli are also the same.
Here is a possible way to fix this using named module types (also known as
signatures). First, delete the files lib/cumulus/m.mli and lib/status/m.mli.
Then modify module Wmo interface and implementation.
wmo.mli
module type Nimbus = sig
val nimbus : string
end
module Cumulus : Nimbus
module Stratus : Nimbus
wmo.ml
module type Nimbus = sig
val nimbus : string
end
module Cumulus = Cumulus.M
module Stratus = Stratus.M
This result is the same, except implementations Cumulus.M and Stratus.M are
explicitly bound to the same interface, defined in module Wmo.
Disable Library Wrapping
This section details how Dune wraps a library's contents into a dedicated module. It also shows how to disable this mechanism.
The lib folder contents are trimmed down back to a state close to what it was
in the Libraries section. Delete file lib/cumulus/m.ml,
lib/stratus/m.ml, lib/wmo.mli, and lib/wmo.ml. Here are the contents of the
only files we need:
lib/dune
(library (name wmo))
lib/cumulus.ml
let nimbus = "Cumulonimbus (Cb)"
let altus = "Altocumulus (Ac)"
lib/stratus.ml
let nimbus = "Nimbostratus (Ns)"
In this setup, running dune utop allows discovering what's available.
# #show Wmo;;
module Wmo : sig module Cumulus = Wmo.Cumulus module Stratus = Wmo.Stratus end
# #show Wmo.Cumulus;;
module Cumulus : sig val nimbus : string val altus : string end
# #show Wmo.Stratus;;
# module Stratus : sig val nimbus : string end
# #show Wmo__Cumulus;;
module Wmo__Cumulus : sig val nimbus : string val altus : string end
# #show Wmo__Stratus;;
# module Stratus : sig val nimbus : string end
Five modules are defined. Wmo is the wrapper module having Cumulus and
Stratus as submodules. Compilation of files lib/cumulus.ml and
lib/stratus.ml respectively produce modules Wmo__Cumulus and Wmo__Stratus.
The former submodules of Wmo are respective aliases of the latter.
The wrapper Wmo can be written manually. This one illustrates how a wrapped
submodule's interface can be restrained.
lib/wmo.ml
module Cumulus : sig val nimbus : string end = Cumulus
module Stratus = Stratus
Here is what it looks like in dune utop:
# #show Wmo.Cumulus;;
module Cumulus : sig val nimbus : string end
# #show Wmo__Cumulus;;
module Wmo__Cumulus : sig val nimbus : string val altus : string end
Wrapping can be disabled in Dune's configuration.
lib/dune
(library (name wmo) (wrapped false) (modules cumulus stratus))
In that case, the “library” only contains the modules Cumulus and Stratus,
bundled together, side by side. Check the following in dune utop, twice. Once
with file lib/wmo.ml unchanged, and a second time after deleting it.
# #show Cumulus;;
module Cumulus : sig val nimbus : string val altus : string end
# #show Stratus;;
module Stratus : sig val nimbus : string end
Remarks:
- When the file
lib/wmo.mlexists, themodulesstanza that doesn't list it prevents it from being bundled in the library - When the file
lib/wmo.mldoesn't exist, thewrapped falsestanza prevents the creation of theWmowrapper
Conclusion
The OCaml module system allows organising a project in many ways. Dune provides several means to arrange modules into libraries.
Help Improve Our Documentation
All OCaml docs are open source. See something that's wrong or unclear? Submit a pull request.