This tutorial is based on the official OCaml documentation and the video Getting Started with OCaml with Pedro Cattori.... Thank you for creating such an amazing video! More project-oriented tutorials are needed to help developers learn OCaml effectively. This tutorial is my contribution to the community. 🫰
References
Note: This article also includes notes for OCaml maintainers. The OCaml community is welcoming, and I trust that my feedback will be well-received.
OCaml Global Setup
Install OCaml
Start by installing Opam, the OCaml package manager, which is similar to npm in JavaScript. It manages packages and compiler versions.
For macOS
brew install opam
For Linux
sudo apt-get install opam
For Windows
winget install Git.Git OCaml.opam
Setup OCaml Globally
Initialize OCaml's global configuration with Opam:
opam init -y
Activate the Opam global configuration by running:
eval $(opam env)
Note: You need to run eval $(opam env)
every time you open a new terminal to activate the Opam global configuration. Consider adding this command to your .bashrc
or .zshrc
file to automate this process.
Install Platform Tools
Install essential tools to assist your code editor with the OCaml LSP server and help you create and manage OCaml projects with Dune:
opam install ocaml-lsp-server odoc ocamlformat utop
These tools provide:
-
ocaml-lsp-server
: Language server for editor integration (code completion, etc.) -
odoc
: Documentation generator -
ocamlformat
: Code formatter -
utop
: Enhanced REPL (interactive console) -
dune
: Build system and project manager
Create and Run a Project
Create a Project
Dune is OCaml's default build system, automating compilations, tests, and documentation generation. It also helps create and manage projects. If you've run the previous commands, Dune is already installed. Create a new project by running:
dune init proj my_project
cd my_project
Project Structure
Most of your work will happen in the lib/
folder and the main.ml
file, which is the application's entry point. Dune projects primarily consist of:
-
lib/
: Contains your modules (.ml
files). -
bin/
: Contains executable/compiled programs. -
_opam/
: Stores project dependencies. -
_build/
: Contains build artifacts. -
dune-project
: Equivalent topackage.json
in JavaScript orrequirements.txt
in Python. -
bin/main.ml
: The application’s entry point.
Create a Switch for Your Project
Switches in OCaml are similar to Python's virtual environments. They isolate compilers and package versions per project, preventing conflicts.
List available compiler versions:
opam switch list-available
Create a switch with your selected version:
opam switch create . 5.3.0 --deps-only
This command creates and stores switch artifacts in the _opam/
folder. The --deps-only
flag ensures that only dependencies are installed, not the current project.
Enable Automatic Switch Detection
Run this command once to help Opam automatically detect if you are in the right switch:
opam init --enable-shell-hook
Activate the Switch
Activate the switch every time you enter the project:
eval $(opam env)
Install Dev Tools for the New Switch
This step is necessary, especially if you use a modal code editor like Vim:
opam install ocaml-lsp-server odoc ocamlformat
Run the Project
Compile and execute your project:
dune build
dune exec my_project
Alternatively, use watch mode to accomplish both tasks with a single command:
dune exec -w my_project
This creates the _build/
folder containing OCaml's compiled artifacts.
Create the .gitignore
File
Dune projects do not include a .gitignore
file by default. Create it manually:
# .gitignore
_opam/
_build/
Initialize Git
git init
Congratulations! You have created a new project. Next, we will create a simple program using OCaml and Dune features.
Create Your First Module
Let's create a simple calculator with add
and sub
functions to learn about Dune.
Create the Module
In OCaml, each .ml
file in the lib/
folder is considered a module. Create calc.ml
and add the following functions:
(* lib/calc.ml *)
let add x y = x + y
let sub x y = x - y
Define a Library
Define everything inside the lib/
folder as a library named math
by creating a dune
file in lib/
:
; lib/dune
(library
(name math))
Libraries in OCaml are similar to index files in JavaScript; they expose modules.
Register the Library in bin/dune
To access your library from main.ml
, open the dune
file in the bin/
folder and register the library name:
; bin/dune
(executable
(public_name ocaml_dune)
(name main)
(libraries math)) ; Include your module here
Use the Module in main.ml
Access the library in main.ml
using the open
keyword, and call your module definitions:
(* bin/main.ml *)
open Math
let () =
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 *)
Notice that we 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
Interface Files (.mli
)
.mli
files are OCaml interface files. They serve as:
- Module interface definition: Define the public interface of a module.
- Encapsulation: Prevent accidental exposure of internal module details.
- Documentation: Include comments for clarity.
- Separation of interface and implementation: Change module internals without affecting other program parts.
- Improve compilation time: The compiler relies on them to speed up.
Define the Module’s Public API
Create an .mli
file with the same name as its corresponding module. For calc.ml
, create calc.mli
in the lib/
folder:
(* lib/calc.mli *)
val add : int -> int -> int
(** [add x y] returns the sum of x and y. *)
If you attempt to use sub
in your running program, it will result in an error. Expose sub
in calc.mli
to fix this:
(* 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. *)
Add a Dependency
Let's add ANSITerminal, a lightweight package for colored terminal output.
Subscribe the Module in dune-project
Edit your dune-project
file and add the dependency name in the depends
node:
...
(package
(name my_project)
(depends
ocaml
(ANSITerminal (>= 0.8.5))) ; add package here
Install the Module
Run dune build
to automatically install the dependency:
dune build
Alternatively, you can run:
opam install ANSITerminal
However, dune build
is recommended because it creates the ocaml_dune.opam
file, similar to package.lock.json
in JavaScript, maintaining a register of the exact package versions.
Import the Module
Update the dune
file in the bin/
folder to include the new dependency:
(executable
(public_name ocaml_dune)
(name main)
(libraries math ANSITerminal)) ; add package here
Update main.ml
(* 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"
Testing
Alcotest is a simple and efficient testing framework for OCaml. It provides a straightforward way to write and run tests, making it easier to ensure the reliability and correctness of your code. This is the tool we are going to use.
Add Alcotest to dune-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
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 the Dependency
After updating the dune-project
file, install the dependency by running:
opam install alcotest
And then:
dune build
This registers the dependency in the ocaml_dune.opam
file, which serves a similar purpose to package.lock.json
in other ecosystems.
Register Alcotest in the Test dune
File
Next, you need to register Alcotest and your libraries for your tests. This is done in the test/dune
file:
(test
(name test_my_project)
(libraries alcotest math)) ; Include Alcotest and your math library
By doing this, any .ml
files in the test/
folder will have access to both Alcotest and your library's modules.
Create Your First Dummy Test
Now, create a simple test to ensure everything is set up correctly. 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;
];
]
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
Next Steps
- Explore the standard library in the OCaml Manual
- Try Real World OCaml
- Browse packages on OCaml Packages
Happy coding with OCaml! 🚀
Feedback for OCaml Maintainers
- A direct link to available compiler versions in the switch documentation would be helpful.
-
--deps-only
: Installing packages without considering the current project could be the default behavior to simplify the setup. -
eval $(opam env)
: Automating this command every time a switch is activated would improve the developer experience. -
dune exec -w my_project
: Avoid writing the project name to run the project, simplifying the setup. - 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 thedune-project
and update theocaml_dune.opam
file. - It would be great if we didn't have to define a
dune
file for each module, and instead, this would be the default behavior