DEV Community

david2am
david2am

Posted on

OCaml/Dune Modules & Libraries (Like JS but Better)

Tired of OCaml setup hell?

In 5 minutes, you’ll have:

  • A working project

Just copy, paste, and run.

Index


🟣 Use Modules and Libraries

Let's add some modules, interface files, tests, and more!

Create a Module

In OCaml the concept of module is similar to the one in Python or JavaScript where every file is considered an independent unit of work with its own namespace.

Create a calc.ml file in the lib/ folder and add the following functions:

(* lib/calc.ml *)
let add x y = x + y

let sub x y = x - y
Enter fullscreen mode Exit fullscreen mode

Create a dune File

In Dune it's not enough to create a file to use it as a module, you need to explicitly say it through adding metadata in a dune file.

But before setting the metadata it's important to differentiate between a module and a library in OCaml:

  • Module = single unit of code organization. They form what it's called a compilation unit, a fully independent program from the compiler's perspective
  • Library = packaged collection of modules for distribution

In this occasion you will define everything inside the lib/ folder as a library named math so you could later call the Calc module through it.

Create a dune file in lib/:

; lib/dune
(library
 (name math))
Enter fullscreen mode Exit fullscreen mode

Libraries in OCaml are similar to index files in JavaScript; they expose modules.

Register a Library

In a similar way, if you want to consume a library in another .ml file you need to register it in its adjacent dune file to make it accessible, let's make it accessible to the bin/main.ml file:

In the bin/ folder open the dune file and register the library by its name:

; bin/dune
(executable
 (public_name ocaml_dune)
 (name main)
 (libraries math)) ; Include your module here
Enter fullscreen mode Exit fullscreen mode

Use a Library

Use the open keyword to access the library in main.ml:

(* bin/main.ml *)
open Math

let () = (* in other programming languages this would be to your main function equivalent *)
  let result = Calc.add 2 3 in
  print_endline (Int.to_string result); (* Output: 5 *)
Enter fullscreen mode Exit fullscreen mode

Notice that you also use the Int module from the standard library to convert a number to a string.

Run the Project

Compile and execute your project in watch mode:

dune exec -w my_project
Enter fullscreen mode Exit fullscreen mode

🟣 Create an Interface File

In Dune it's possible to separate your interfaces from your implementation through .mli files. They serve as a way for:

  • Encapsulation: helps define the public interface of a module.
  • Hide implementation details: makes possible to change module internals without affecting other dependent parts.
  • Documentation: it's a good practice to include specifications for clarity.

In the lib/ folder create an .mli file with the same name as its corresponding module, calc.mli in this case:

(* lib/calc.mli *)
val add : int -> int -> int
(** [add x y] returns the sum of x and y. *)
Enter fullscreen mode Exit fullscreen mode

Run the Project

Run:

dune exec -w my_project
Enter fullscreen mode Exit fullscreen mode

Add the sub function to the main.ml file

(* bin/main.ml *)
open Math

let () = (* in other programming languages this would be to your main function equivalent *)
  let result = Calc.add 2 3 in
  print_endline (Int.to_string result); (* Output: 5 *)

  let result = Calc.sub 3 1 in
  print_endline (Int.to_string result) (* Output: 2 *)
Enter fullscreen mode Exit fullscreen mode

As you see if you attempt to use sub it will result in an error, it's because the sub function has not been exposed yet, to solve it add the sub interface in calc.mli:

(* lib/calc.mli *)
val add : int -> int -> int
(** [add x y] returns the sum of x and y. *)

val sub : int -> int -> int
(** [sub x y] returns the difference of x and y. *)
Enter fullscreen mode Exit fullscreen mode

🟣 Add a Dependency

Let's add ANSITerminal, a lightweight package for colored terminal output.

Subscribe the Module in your Project

If you remember your dune-project file works as a registry for your project dependencies (as package.json if you are familiar with JavaScript).

Add the dependency in the depends node:

...
(package
 (name my_project)
 (depends
  ocaml
  (ANSITerminal (>= 0.8.5))) ; add package here
Enter fullscreen mode Exit fullscreen mode

Install the Module

Run:

opam install ANSITerminal
Enter fullscreen mode Exit fullscreen mode

Run:

dune build
Enter fullscreen mode Exit fullscreen mode

dune build creates the ocaml_dune.opam file, similar to package.lock.json in JavaScript to register the exact package versions.

Add It to the dune File

In the dune file in the bin/ folder do:

(executable
 (public_name ocaml_dune)
 (name main)
 (libraries math ANSITerminal)) ; add package here
Enter fullscreen mode Exit fullscreen mode

Use It

Update you main.ml file in this way:

(* Import the module *)
open ANSITerminal

(* Print nicely formatted text in the terminal *)
let () =
  print_string [Bold; green] "\n\nHello in bold green!\n";
  print_string [red] "This is in red.\n"
Enter fullscreen mode Exit fullscreen mode

Testing

Alcotest is a simple and efficient testing framework for OCaml. It provides a straightforward way to write and run tests ensuring reliability and correctness. This is the tool we are going to use.

Add Alcotest to your Project Dependencies

To integrate Alcotest into your project, you need to add it as a dependency in your dune-project file. This ensures that Alcotest is available for your test suite.

(package
 (name my_project)
 (depends
  ocaml
  (ANSITerminal (>= 0.8.5))
  (alcotest :with-test))) ; Add Alcotest as a test-only dependency
Enter fullscreen mode Exit fullscreen mode

The :with-test constraint specifies that Alcotest is a test-only dependency, meaning it will only be used during testing and not included in the final build.

Install Alcotest

After updating the dune-project file, install the dependency by running:

opam install alcotest
Enter fullscreen mode Exit fullscreen mode

And then:

dune build
Enter fullscreen mode Exit fullscreen mode

This registers the dependency in the ocaml_dune.opam file, which serves a similar purpose to package.lock.json in other ecosystems.

Register Alcotest

Register Alcotest and your libraries in the test/dune file:

; test/dune
(test
 (name test_my_project)
 (libraries alcotest math)) ; Include Alcotest and your math library
Enter fullscreen mode Exit fullscreen mode

By doing this, any .ml files in the test/ folder will have access to both Alcotest and your library's modules.

Create a Test

Add the following code to the test/test_my_project.ml file:

(* test/test_my_project.ml *)
let test_hello () =
  Alcotest.(check string) "same string" "hello" "hello"

let () =
  Alcotest.run "My Project" [
    "hello", [
      Alcotest.test_case "Hello test" `Quick test_hello;
    ];
  ]
Enter fullscreen mode Exit fullscreen mode

This dummy test checks if the string "hello" is equal to "hello", which should always pass. It's a good starting point to verify that your testing setup is functional.

Run the Test

dune test
Enter fullscreen mode Exit fullscreen mode

Next Steps

Did this help?

  • Give it a ❤️
  • Follow for Part 3: Coming soon
  • Share with one friend learning systems programming

Have a question? Comment below — I reply to all!


References


Feedback for OCaml Maintainers

  • Consider adding a direct link to available compiler versions in the switch documentation, it would be helpful.
  • --deps-only: consider this the default behavior so the developer skip this flag.
  • opam init -y: consider making it the default so the developer skip this step.
  • opam init --enable-shell-hook: consider making it the default so the developer skip this step.
  • dune exec -w my_project: consider avoiding writing the project name to run the project.
  • Automatically create a .gitignore file to avoid manual setup.
  • We should include a dune CLI command to install a package, register the library name in the dune-project and update the ocaml_dune.opam file all at once.

Top comments (0)