tech

Getting started with OCaml and Functional Programming

Get started writing functional programming code using the OCaml programming language: install OCaml, create a new project using dune, learn how to write functional programming code.


Sandro Maglione

Sandro Maglione

Software development

Get started writing functional programming code using OCaml:

  • Install OCaml and setup a full developer environment (formatting, compiling, auto complete)
  • Learn how to create an OCaml project using dune
  • Learn how to write functional code using OCaml (pattern matching, types, piping)
Open Source Repository

Installing OCaml

These are the steps I followed to install OCaml:

  1. Make sure gcc, build-essential, curl, unzip, and bubblewrap are installed
gcc --version
curl --version
unzip --version
  1. Run the install command
bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"

The next step is initializing opam:

opam init

opam (OCaml Package Manager) is a package manager for OCaml (similar to npm for javascript)

It allows to install and manage packages and dependencies.

This process will ask you to setup the ocaml command in PATH:

<><> Required setup - please read <><><><><><><><><><><><><><><><><><><><><><><>

  In normal operation, opam only alters files within ~/.opam.

  However, to best integrate with your system, some environment variables
  should be set. If you allow it to, this initialisation step will update
  your zsh configuration by adding the following line to ~/.zshrc:

    [[ ! -r /Users/sandromaglione/.opam/opam-init/init.zsh ]] || source /Users/sandromaglione/.opam/opam-init/init.zsh  > /dev/null 2> /dev/null

  Otherwise, every time you want to access your opam installation, you will
  need to run:

    eval $(opam env)

  You can always re-run this setup with 'opam init' later.

Do you want opam to modify ~/.zshrc? [N/y/f]
(default is 'no', use 'f' to choose a different file) y

It will then install the compiler and all relative dependencies:

<><> Creating initial switch 'default' (invariant ["ocaml" {>= "4.05.0"}] - initially with ocaml-base-compiler) 

<><> Installing new switch packages <><><><><><><><><><><><><><><><><><><><><><>
Switch invariant: ["ocaml" {>= "4.05.0"}]

<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><>
-> installed base-bigarray.base
-> installed base-threads.base
-> installed base-unix.base
-> installed ocaml-options-vanilla.1
-> retrieved ocaml-base-compiler.5.1.0  (https://opam.ocaml.org/cache)
-> installed ocaml-base-compiler.5.1.0
-> installed ocaml-config.3
-> installed ocaml.5.1.0
-> installed base-domains.base
-> installed base-nnp.base
Done.
# Run eval $(opam env --switch=default) to update the current shell environment

Setting up developer environment

I also installed some other dependencies suggested to run a productive developer environment. These are tools that take care of compilation, auto completion, formatting:

opam install dune merlin ocaml-lsp-server odoc ocamlformat utop dune-release

This command will install some new dependencies:

The following actions will be performed:
  - install cmdliner            1.2.0    [required by ocamlformat,
                                         dune-release, odoc]
  - install ocamlfind           1.9.6    [required by utop]
  - install seq                 base     [required by yojson, tyxml]
  - install ocamlbuild          0.14.2   [required by bos, astring, uutf]
  - install dune                3.12.1
  - install base-bytes          base     [required by ocp-indent]
  - install uchar               0.0.2    [required by zed]
  - install topkg               1.0.7    [required by bos, astring, uutf]
  - install xdg                 3.12.1   [required by ocaml-lsp-server, utop]
  - install trie                1.0.0    [required by mew]
  - install stdlib-shims        0.3.0    [required by ocamlgraph]
  - install spawn               v0.15.1  [required by ocaml-lsp-server]
  - install sexplib0            v0.16.0  [required by base]
  - install result              1.5      [required by odoc]
  - install re                  1.11.0   [required by ocaml-lsp-server,
                                         dune-release, ocamlformat]
  - install pp                  1.2.0    [required by ocaml-lsp-server]
  - install ordering            3.12.1   [required by ocaml-lsp-server]
  - install opam-file-format    2.1.6    [required by dune-release]
  - install ocaml-version       3.6.2    [required by ocamlformat-lib]
  - install menhirSdk           20230608 [required by ocamlformat-lib]
  - install menhirLib           20230608 [required by ocamlformat-lib]
  - install fix                 20230505 [required by ocamlformat-lib]
  - install either              1.0.0    [required by ocamlformat-lib]
  - install dune-build-info     3.12.1   [required by ocaml-lsp-server]
  - install csexp               1.5.2    [required by ocaml-lsp-server]
  - install cppo                1.6.9    [required by odoc, utop]
  - install chrome-trace        3.12.1   [required by ocaml-lsp-server]
  - install camlp-streams       5.0.1    [required by odoc-parser,
                                         ocamlformat-lib]
  - install bigarray-compat     1.1.0
  - install ocp-indent          1.8.1    [required by ocamlformat-lib]
  - install uutf                1.0.3    [required by ocaml-lsp-server]
  - install uucp                15.1.0   [required by uuseg, zed]
  - install rresult             0.7.0    [required by dune-release]
  - install react               1.2.2    [required by utop]
  - install fmt                 0.9.0    [required by dune-release, odoc]
  - install astring             0.8.5    [required by dune-release, odoc]
  - install ocamlgraph          2.1.0    [required by opam-core]
  - install mew                 0.1.0    [required by mew_vi]
  - install curly               0.3.0    [required by dune-release]
  - install dyn                 3.12.1   [required by ocaml-lsp-server]
  - install menhir              20230608 [required by ocamlformat-lib]
  - install ocamlformat-rpc-lib 0.26.1   [required by ocaml-lsp-server]
  - install merlin-lib          4.12-501 [required by merlin, ocaml-lsp-server]
  - install dune-configurator   3.12.1   [required by base]
  - install yojson              2.1.2    [required by merlin, dune-release,
                                         ocaml-lsp-server]
  - install ocplib-endian       1.2      [required by lwt]
  - install tyxml               4.6.0    [required by odoc]
  - install uuseg               15.1.0   [required by ocamlformat-lib]
  - install odoc-parser         2.0.0    [required by ocaml-lsp-server, odoc]
  - install fpath               0.7.3    [required by dune-release, odoc]
  - install opam-core           2.1.5    [required by dune-release]
  - install mew_vi              0.5.0    [required by lambda-term]
  - install stdune              3.12.1   [required by ocaml-lsp-server]
  - install ocamlc-loc          3.12.1   [required by ocaml-lsp-server]
  - install dot-merlin-reader   4.9      [required by merlin]
  - install base                v0.16.3  [required by ocamlformat-lib]
  - install ppx_yojson_conv_lib v0.16.0  [required by ocaml-lsp-server]
  - install lwt                 5.7.0    [required by utop]
  - install zed                 3.2.3    [required by utop]
  - install odoc                2.2.2
  - install opam-format         2.1.5    [required by dune-release]
  - install fiber               3.7.0    [required by ocaml-lsp-server]
  - install dune-rpc            3.12.1   [required by ocaml-lsp-server]
  - install merlin              4.12-501
  - install stdio               v0.16.0  [required by ocamlformat-lib]
  - install lwt_react           1.2.0    [required by utop]
  - install logs                0.7.0    [required by dune-release, utop]
  - install opam-repository     2.1.5    [required by opam-state]
  - install ocaml-lsp-server    1.16.2
  - install ocamlformat-lib     0.26.1   [required by ocamlformat]
  - install lambda-term         3.3.2    [required by utop]
  - install bos                 0.2.1    [required by dune-release]
  - install opam-state          2.1.5    [required by dune-release]
  - install ocamlformat         0.26.1
  - install utop                2.13.1
  - install dune-release        2.0.0
===== 76 to install =====

Finally, I installed the OCaml Platform extension for VSCode.

This is all to start being productive with OCaml on VSCode.

There is more 🤩

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

Create your first OCaml program

An OCaml file ends with the .ml extension. You can create a single main.ml file and add some code:

main.ml
let () = print_endline "Hello, World!"

You can then run the ocaml command to execute it:

ocaml main.ml

This is your first program in OCaml: "Hello, World!".

Create a project using dune

dune is the standard build system for OCaml.

dune allows to create a full OCaml project and build executables, libraries, run tests, and much more.

We can create a new project running the dune init command:

# `example` is the name of the project
dune init proj example
  • _build: Built files (after running dune build)
  • bin: Executable file
  • lib: Modules
  • test: Testing

Initial project structure after running dune initInitial project structure after running dune init

We can then compile the project running the build command:

dune build

dune will compile the project and generate some files inside the _build folderdune will compile the project and generate some files inside the _build folder

Finally, we can execute the program by running the exec command:

dune exec example

This is the standard workflow when working with dune:

  1. Write implementation inside modules in lib
  2. Import and use modules for executables inside bin
  3. Run the dune build command
  4. Run the dune exec ... command

Code formatting

We need to create a new .ocamlformat file to enable auto-formatting:

.ocamlformat
profile = janestreet
version = 0.26.1

Now we can execute the formatting command:

dune fmt # or dune build @fmt

Final dune project setup and files

The final project contains the following files:

  • dune-project: Defines general config and information
  • aoc_ocaml.opam: Autogenerated from dune-project, do not touch!
  • .ocamlformat: Adds formatting. Define the style type as profile
  • lib/dune/dune-project: Modules configurations, name defines the export to use inside bin (in the example name aoc means that you use Aoc as module inside bin)
lib/dune/dune-project
(library
 (name aoc))
  • bin/dune/dune-project: Execution configuration
    • public_name defines the command to run with exec (in the example public_name aoc means calling dune exec aoc)
    • name references the name of the entry file to execute (in the example name main means executing the main.ml file)
    • libraries references the name assigned in the lib/dune/dune-project file
bin/dune/dune-project
(executable
 (public_name aoc)
 (name main)
 (libraries aoc))

Final project configuration for a complete dune programFinal project configuration for a complete dune program

With this setup we run the following commands in sequence to format, build, and execute the program:

dune fmt # Format
dune build # Build
dune exec aoc # Execute

There is more 🤩

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

OCaml and Functional Programming

Any OCaml program is based on functions composed together.

You define a function using let followed by the function name and parameters:

let sum a b = a + b

OCaml is very smart in inferring types for you, no need to write them manually.

In the sum function OCaml knows that a and b are int because we use the + operator (which works on int types).

OCaml embraces functional programming. The key features are:

  • All variables are immutable
  • "Mostly pure" (OCaml allows side effect through things like references and arrays)
  • Function composition
  • Functional types like Option and Either
  • Powerful pattern matching

Write OCaml code

Let's solve a puzzle to learn how to write OCaml code:

Given a string, combine the first and last digit to form a single two-digit number.

For example 1abc2 (12), pqr3stu8vwx (38), a1b2c3d4e5f (15), treb7uchet (77).

This puzzle comes from Advent of code 2023 (day 1)

When working with functional programming you usually start by defining types (states).

In this example our program can be in 2 states:

  • No digit yet found (Empty)
  • Digits found (Full)

We can implement this in OCaml using type:

type digits =
  | Empty
  | Full of int * int
  • digits: Name of the type
  • Empty: State without parameters (notice how we did not add any type after it)
  • Full: State that contains a pair of int (int * int represents a tuple, meaning a value that requires 2 int, also called Product Type, hence the * syntax)

We can now create instances of the digits type using Empty and Full:

let empty = Empty
let full = Full (1, 2)

Pattern matching

Another feature that is powerful and used everywhere in OCaml and functional programming is pattern matching.

For example we can pattern match on a value of type digits.

Since the definition of digits allows for only 2 states, OCaml will make sure that we matched all possible states.

Pattern matching in OCaml uses the match ... with syntax:

let digits_sum dig =
  match dig with
  | Empty -> 0
  | Full (firstNum, lastNum) -> (firstNum * 10) + lastNum

The digits_sum function takes a dig of type digits as input (the type is inferred by OCaml because we are matching on Empty and Full).

We then use match to return some value for each state:

  • Empty: No digits available, return 0
  • Full: We have both digits so we can combine them

For example from the input 1abc2 we will extract Full (1, 2), and then using digits_sum: (1 * 10) + 2 = 12

When we have a single input that we pattern match directly we can shorten the code by using function:

let digits_sum = function
  | Empty -> 0
  | Full (firstNum, lastNum) -> (firstNum * 10) + lastNum

Collecting digits

Solving the problem requires to iterate over all the characters in the string and extract the first and last digit.

We can achieve this using digits and pattern matching. We define a new collect_digits function that takes 2 parameters:

  • dig: The current digits value (initially Empty)
  • str: The current character (of type string)
let collect_digits dig str =
  let int_option = int_of_string_opt str in
  match dig, int_option with
  | Empty, None -> Empty
  | Empty, Some n -> Full (n, n)
  | Full (f, l), None -> Full (f, l)
  | Full (f, _), Some n -> Full (f, n)

First we use the int_of_string_opt function to convert str to int option.

let collect_digits dig str =
  let int_option = int_of_string_opt str in
  match dig, int_option with
  | Empty, None -> Empty
  | Empty, Some n -> Full (n, n)
  | Full (f, l), None -> Full (f, l)
  | Full (f, _), Some n -> Full (f, n)

Here option will be:

  • None if str cannot be converted to int
  • Some containing str converted to int when str represents a valid number
type option = 
  | None
  | Some int

You can learn more about the Option type here: Functional Programming Option type - Introduction

We then pattern match on both dig and int_option:

  • Empty + None: No digits yet, and no new digit found, return Empty
  • Empty + Some: No digits yet, and we found a new one, return Full containing the found digit n
  • Full + None: We already have some digits, and no new digit found, return same Full
  • Full + Some: We already have some digits, and we found a new one, therefore update the last digit we the new one found
let collect_digits dig str =
  let int_option = int_of_string_opt str in
  match dig, int_option with
  | Empty, None -> Empty
  | Empty, Some n -> Full (n, n)
  | Full (f, l), None -> Full (f, l)
  | Full (f, _), Some n -> Full (f, n)

Using pattern matching we can be sure that we defined all possible combinations.

We have 4 combinations: 2 digits (Empty, Full) x 2 option (None, Some).

OCaml will inform us if we forget to match a possible case.

Folding in functional programming

We now need to extract each character from an input string.

We can do this using String.fold_left:

let read_chars source =
  String.fold_left (fun dig chr -> collect_digits dig (Char.escaped chr)) Empty source
  • fun x -> ... represent an anonymous function (lambda)
  • Calling a function in OCaml is done by writing the name of the function (collect_digits) followed by its parameters (no need of parenthesis)
  • Char.escaped is used to convert chr (of type char) to a string
  • We pass Empty as initial state for folding

String.fold_left will iterate over all characters inside source from left to right and provide them as char inside fun.

For each character we call collect_digits. read_chars will therefore return a single digits value containing the digits extracted from the string.

Piping

Finally we can compose all the function we defined to create the final program.

We use the pipe operator |> to achieve this:

let program source = source |> read_chars |> digits_sum

program takes the string source as input. Using |> we apply source to read_chars, the result is then passed to digits_sum.

The above code is equivalent to the following:

let program line = digits_sum (read_chars line)

Function composition is at the core of functional programming and also in OCaml.

Every program is a series of small function composed together to create the final result.

Complete code

Done! The final solution is the following:

example.ml
(* types *)
type digits =
  | Empty
  | Full of int * int

(* pattern matching *)
let digits_sum = function
  | Empty -> 0
  | Full (firstNum, lastNum) -> (firstNum * 10) + lastNum
;;

(* tuple, pattern matching and option *)
let collect_digits dig str =
  let int_option = int_of_string_opt str in
  match dig, int_option with
  | Empty, None -> Empty
  | Empty, Some n -> Full (n, n)
  | Full (f, l), None -> Full (f, l)
  | Full (f, _), Some n -> Full (f, n)
;;

(* folding *)
let read_chars source =
  String.fold_left (fun dig chr -> collect_digits dig (Char.escaped chr)) Empty source
;;

(* piping *)
let program source = source |> read_chars |> digits_sum

This is it!

You have now a complete configuration to start writing your next OCaml project. You also learned how functional programming works and how to write functional code in OCaml.

If you are interested to learn more, every week I publish a new open source project and share notes and lessons learned in my newsletter. You can subscribe here below 👇

Thanks for reading.

👋・Interested in learning more, every week?

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.