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/dir
for each folder the source files are kept in. If you don't add these-I
paths, 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 module
error. So, be sure to provide those-I
paths to the compiler so it will find your source files.