Tutorial/cheat sheat for running/compiling OCaml.
The OCaml package manager is OPAM.
To get OPAM, you can pull an official OCaml Docker container
that has OPAM already installed (see below for details on using
Docker), or you can install it yourself. You can install it on
Ubuntu with apt install opam, you can install it on Mac with
brew install opam, and so on.
By default, OPAM operates per-user, and it stores everything
it installs in your home folder under a folder called ~/.opam.
OPAM lets you switch between different versions of the OCaml compiler. For instance, I can switch to use OCaml 4.06.1:
opam switch 4.06.1
If you haven't used 4.06.1 before, OPAM will build it and
store it in your ~/.opam folder. It will also ask if you
would like it to activate 4.06.1 in your .bash_profile, so
that whenever you log in, you will be using 4.06.1. If you
tell it not to modify your .bash_profile, you can switch
to 4.06.1 any time by typing:
opam switch 4.06.1
And then you can activate it:
eval `opam config env`
At this point, 4.06.1 should be the active version of OCaml:
ocaml -version
You can switch to another version if you like, e.g., 4.05.0:
opam switch 4.05.0
OPAM will build this version if it's not already in your
~/.opam folder, and again it will ask you if you want
it to activate 4.05.1 in your .bash_profile. If you don't
want to use 4.05.0 all the time, you can say No and then
activate 4.05.0 manually by typing:
eval `opam config env`
Now 4.05.0 should be the active version of OCaml:
ocaml -version
Once you have activated an OCaml compiler, you can install OCaml packages. First make sure OPAM has an up-to-date list of packages:
opam update
To upgrade anything:
opam upgrade
Install packages in the obvious way. For instance, to install
the ounit package (for unit tests):
opam install ounit
OPAM will install ounit into your ~/.opam folder, underneath
the 4.05.1 compiler. So packages are installed under particular
compilers. If you switch to 4.06.1, ounit will not be available.
But you can install it under 4.06.1 too, if you like:
opam installl ounit
If you don't need to install any OCaml packages, you can use any of the official OCaml docker images directly.
First, download a particular image, e.g. the alpine version:
docker pull ocaml/opam:alpine
Then get a bash prompt inside the container:
docker run --rm -ti -v $(pwd):/srv -w /srv ocaml/opam:alpine bash
Check the OCaml version:
ocaml -version
OPAM is installed too:
opam --version
You can start the OCaml REPL if you like:
ocaml
Try out some commands:
1 + 1;;
Printf.printf "Hello\n%!";;
Exit the OCAML REPL by typing this:
#quit;;
Exit the whole container by typing this:
exit
The official OCaml Docker images have OPAM installed, but they
are set up without any remote URL for updates. So, if you type
opam update from inside a container, it will not refresh
your list of packages with anything new.
To remedy this, you can add the official remote URL to your own
container. Create a Dockerfile with these contents:
FROM ocaml/opam:alpine
RUN opam repo add opam https://opam.ocaml.org/; \
opam update; \
opam upgrade
Then build it:
docker build -t myocaml:latest .
Now you can run your container:
docker run --rm -ti -v $(pwd):/srv -w /srv myocaml:latest bash
Now when you update, OPAM will pull down new packages from the remote repository you added:
opam update
OCaml comes with two compilers: one (ocamlc) creates bytecode,
and the other (ocamlopt) creates native machine code.
Check out both compilers:
ocamlc -version
ocamlc --help
ocamlopt -version
ocamlopt --help
Bytecode compiles quickly, but the resulting program will only run on machines that have OCaml installed. Native code takes a bit longer to compile, but of course the final program is a distributable binary, and it can run very fast.
Create a file called helloworld.ml:
let () = print_endline "Hello world"
Compile it:
ocamlc -c helloworld.ml
This produces two files:
helloworld.cmi-- a "compiled module interface" file.helloworld.cmo-- a "compiled module object" file.
The ocamlc compiler generates the .cmi file automatically
if you haven't written one yourself.
Link it:
ocamlc -o helloworld.byte helloworld.cmo
Run it:
./helloworld.byte
You can name the linked program anything you like. It needn't
end in *.byte. It's just a convention of the OCaml community
to name bytecode programs *.byte.
This time around, use ocamlopt to compile the helloworld.ml file:
ocamlopt -c helloworld.ml
This produces three files:
helloworld.cmi-- a "compiled module interface" file.helloworld.cmx-- a "compiled module native" object file.helloworld.o-- an object file with machine code.
The ocamlopt compiler generates the .cmi file automatically
if you haven't written one yourself.
Note that ocamlopt generates a bytecode version of the cmi file.
Module interface files are always bytecode.
Link it:
ocamlopt -o helloworld.native helloworld.cmx
Run it:
./helloworld.native
You can name the linked binary anything you like. It needn't end in *.native.
That's just a naming convention of the OCaml community.
In a new folder somewhere, create a folder bin and lib:
mkdir -p bin
mkdir -p lib
Add a .gitignore with these contents:
_build
*~
*.swp
Create a file lib/arithmetic.ml with these contents:
(** A toy library. *)
(* [add n m] adds [n] and [m] together. *)
let add (n : int) (m : int) : int = n + m
(* [sum [a; b; c]] returns the sum of [a], [b], and [c]. *)
let sum (l : int list) : int = List.fold_left add 0 l
Create a file lib/arithmetic.mli with these contents:
(** This module has some toy arithmetic operations. *)
(* [sum l] sums the values of [l]. E.g. [sum [1; 2; 3]] returns [6]. *)
val sum : int list -> int
Create a file bin/main.ml with these contents:
(** This module defines a toy command-line tool.
It doesn't really do anything. It is just a front-end
for the toy {Arithmetic} library (it calls a function
from {Arithmetic} and prints the result).
*)
(* The entry point to the program. *)
let () =
let result = Arithmetic.sum [1; 3; 7; 14] in
Format.printf "The sum of 1, 3, 7, 14 is this: %d\n%!" result
Create a Makefile:
lib_src_dir := lib
bin_src_dir := bin
build_dir := _build
include_dirs := -I $(build_dir)/lib -I $(build_dir)/bin
locally_built_exe := _build/main.exe
installed_exe := /usr/local/bin/hello_2
compiler := ocamlopt
options := -w a
.DEFAULT_GOAL := all
all: uninstall clean build install
.PHONY: clean
clean:
rm -rf $(build_dir)
$(build_dir):
mkdir -p $(build_dir)
cp -R $(lib_src_dir) $(build_dir)/
cp -R $(bin_src_dir) $(build_dir)/
build: $(build_dir) lib/arithmetic.mli lib/arithmetic.ml bin/main.ml
$(compiler) $(options) -intf $(build_dir)/lib/arithmetic.mli
$(compiler) $(options) -o $(locally_built_exe) $(include_dirs) $(build_dir)/lib/arithmetic.ml $(build_dir)/bin/main.ml
.PHONY: install
install: build
sudo install $(locally_built_exe) $(installed_exe)
.PHONY: uninstall
uninstall:
sudo rm -rf $(installed_exe)
Note that this Makefile creates a _build directory and copies all your source files into it, so it can build the artifacts there inside _build. It also compiles the interface file lib/arithmetic.mli first, and then it compiles the rest. Note also that the final call to ocamlopt provides -I arguments (see the comments below for more on why).
Build and install:
make
Try the program:
which hello_2
hello_2
Here are the keys to compiling multiple files:
- Compile each interface file first with
ocamlopt -intf path/to/file.mli - Then compile all implementation files, but include
-I path/to/dirfor each folder the source files are kept in. If you don't add these-Ipaths, the compiler won't find your files, and those modules simply won't be compiled and linked into the final product. The problem is that the compiler fails silently here, so you typically don't discover the problem until you run the final program, and it dies with anUnbound moduleerror. So, be sure to provide those-Ipaths to the compiler so it will find your source files.