OxCaml Parameterised Libraries with Dune

Warning

  • This feature is experimental.

  • Parameterised libraries are only supported by the OxCaml branch of the OCaml compiler. You need to install the OxCaml compiler to use parameterised libraries with Dune.

This tutorial explains how to create and use a parameterized library in Dune. Parameterised libraries specify some of their dependencies as library parameters, without committing a concrete implementations. This is useful for build-time dependency injection and for generalizing over platform-specific implementations. It provides a more flexible alternative to dune’s Virtual Libraries.

By the end of the tutorial, you will know:

  • how to declare a library parameter,

  • how to implement a library parameter,

  • how to parameterise a library on a library parameter,

  • how to instantiate a parameterised library by supplying an implementation of its library parameters.

Getting Started

Once you have checked that OxCaml is installed and its ocamlc is available on your path, you can enable Dune’s oxcaml extension in your dune-project:

(lang dune 3.20)

(using oxcaml 0.1)

Important

Enabling the OxCaml extension by adding the (use oxcaml 0.1) stanza allows dune to use the stanzas and fields specific to OxCaml. This extension is only available since dune 3.20.

Declare a Library Parameter

Library parameters allow libraries to be generic over libraries they depend on. This is useful for dependency injection, testing, platform-specific code, etc.

Note

The interface of a library parameter is specified using an OCaml .mli file and declared via the library_parameter dune stanza. The following analogy holds:

module parameters

library parameters

declaration

module type Intf = <sig>

(library_parameter (name intf)) stanza

specification

a module signature: sig ... end

an interface file: e.g., intf.mli

First, we create an (arbitrarily named) param/ directory where we are going to write our parameter definition.

mkdir param

We create a param/dune file containing a library_parameter stanza that declares a new library parameter named param:

(library_parameter
 (name param))

To complete the declaration, we must provide an .mli file matching the given name, which will specify the interface of the library parameter. We create this in param/param.mli:

type t

val make : unit -> t

val print : t -> unit

This interface specifies the types, functions, and modules which must be provided by a library for it to implement the declared parameter.

Implement a Library Parameter

Note

The implementation of a library parameter must satisfy the signature of the parameter it claims to implement. The following analogy holds:

module implementation

library implementation

ascribed declaration

module Impl : Intf

a library stanza with an implements field: (library (name impl) (implements intf))

implementation

a module expression: struct ... end

a set of .ml files

We create an (arbitrarily named) directory impl/ where we are going to write an implementation of our library parameter:

mkdir impl

An implementation of a declared library parameter named param is a normal dune library with the (implements param) annotation. We declare this impl/dune:

(library
 (name impl)
 (implements param))

For the sake of simplicity, we give the example of a single-module library, defined in impl/impl.ml. This must satisfy the parameter interface ascribed by the implements field:

type t = string

let make () = "impl"

let print t = print_endline t

Define a Parameterised Library

Note

A parameterised library must specify the parameters it requires. The following analogy holds:

parameterised module (a “functor”)

parameterised library

declaration

module M (P : Intf)

a (library (name m) (parameters intf)) stanza

We create an (arbitrarily named) directory lib/ where we are going to write a library that is parameterised on our declared library parameter.

mkdir lib

We add a lib/dune file to declare the library, with the (parameters param) field, indicating that the library is generic over the library parameter param:

(library
 (name lib)
 (parameters param))

Within the modules comprising a parameterised library, we can then refer library parameters without having to decide on a concrete implementation of the libraries up front. In our example, we can directly use the functions from the declared Param in our library lib/lib.ml:

let lib () = Param.print (Param.make ())

Instantiate a Parameterised Library

Note

To instantiate a parameterised library, we must supply library arguments that implement all specified parameters. The following analogy holds:

functor application

library instantiation

instantiation

M (P)

a (instantiate m p) expression in Library Dependencies

A library can depend on parameterised Libraries if it is, either, parameterised with the same parameters as its dependencies, or, it instantiates its parameterised dependencies. An executable, however, must instantiate any parameterised libraries, otherwise we wouldn’t be able to compile a complete program to execute.

To conclude this tutorial, we define an executable target in the new folder bin/, that depends on an instantiation of our parameterised library.

We create an (arbitrarily named) bin directory:

mkdir bin

Then we add a bin/dune file defining our executable:

(executable
 (name bin)
 (libraries
  (instantiate lib impl)))

In the libraries field, the syntax (instantiate lib impl) specifies that we want to use the concrete library resulting from applying the parameterised library lib to the library argument impl. Note that this application is only possible because we defined the impl library as an implementation of the library parameter, param.

The code for our binary simply calls the library code, using the library’s name, Lib:

let () = Lib.lib ()

Conclusion

In this tutorial, we have learned how to declare a library parameter, define its implementation, define a parameterised library, and finally to use the latter by instantiating it for an executable target.