Skip to content

Instantly share code, notes, and snippets.

@jtpaasch
Last active December 17, 2022 13:35
Show Gist options
  • Save jtpaasch/2b2182ad265a219f50b3d08da6586b6b to your computer and use it in GitHub Desktop.
Save jtpaasch/2b2182ad265a219f50b3d08da6586b6b to your computer and use it in GitHub Desktop.
Tutorial/cheat sheet for compiling/running OCaml programs.

Running/Compiling OCaml

Tutorial/cheat sheat for running/compiling OCaml.

Style guideline

Documentation

Package manager (OPAM)

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.

Where OPAM Stores Stuff

By default, OPAM operates per-user, and it stores everything it installs in your home folder under a folder called ~/.opam.

Switching between OCaml versions

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

Installing Packages

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

Using Docker

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

Using OPAM and Docker

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 Compilers

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.

Compiling Bytecode

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.

Compiling Native Code

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.

Building with multiple modules

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 an Unbound module error. So, be sure to provide those -I paths to the compiler so it will find your source files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment