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.
Requirements: Modules and Functors.
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 folder that requires some sort of build must contain a dune
file. The executable
stanza means an executable program is built.
- The
name cloud
stanza means the filecloud.ml
contains the executable. - The
public_name nube
stanza 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 folder contents:
$ tree
.
├── dune
├── dune-project
├── cloud.ml
└── wmo.ml
Dune stores the files it creates in a folder named _build
. In a project managed using Git, the _build
folder should be ignored
$ echo _build >> .gitignore
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:
mixtli
is the project's name (it means cloud in Nahuatl).cloud.ml
is the OCaml source file's name, referred ascloud
in thedune
file.nube
is the executable command's name (it means cloud in Spanish).Cloud
is the name of the module associated with the filecloud.ml
.Wmo
is the name of the module associated with the filewmo.ml
.wmo-clouds
is 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 module do for definitions; they avoid name clashes.
Dune creates libraries from folders. Let's look at an example. Here the folder is lib
:
$ mkdir lib
The lib
folder is populated with the following 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
folder 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
Wmo
from the contents of folderlib
. - The folder's name (here
lib
) is irrelevant. - The library name appears uncapitalised (
wmo
) indune
files:- 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 Cumulus
means moduleWmo
contains a submodule namedCumulus
. - On the right-hand side,
Cumulus
refers 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 folder contains a wrapper module (here wmo.ml
), it is the only one exposed. All other file-based modules from that folder 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 folder as the dune
file, but it doesn't look into subfolders. 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 subfolders containing a dune
file. See the Dune documentation for more on this topic.
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.