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)
Installing OCaml
These are the steps I followed to install OCaml:
- Make sure
gcc
,build-essential
,curl
,unzip
, andbubblewrap
are installed
gcc --version
curl --version
unzip --version
- 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 tonpm
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:
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 runningdune build
)bin
: Executable filelib
: Modulestest
: Testing
Initial 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 folder
Finally, we can execute the program by running the exec
command:
dune exec example
This is the standard workflow when working with
dune
:
- Write implementation inside modules in
lib
- Import and use modules for executables inside
bin
- Run the
dune build
command- Run the
dune exec ...
command
Code formatting
We need to create a new .ocamlformat
file to enable auto-formatting:
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 informationaoc_ocaml.opam
: Autogenerated fromdune-project
, do not touch!.ocamlformat
: Adds formatting. Define the style type asprofile
lib/dune/dune-project
: Modules configurations,name
defines the export to use insidebin
(in the examplename aoc
means that you useAoc
as module insidebin
)
(library
(name aoc))
bin/dune/dune-project
: Execution configurationpublic_name
defines the command to run withexec
(in the examplepublic_name aoc
means callingdune exec aoc
)name
references the name of the entry file to execute (in the examplename main
means executing themain.ml
file)libraries
references thename
assigned in thelib/dune/dune-project
file
(executable
(public_name aoc)
(name main)
(libraries aoc))
Final 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 thata
andb
areint
because we use the+
operator (which works onint
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
andEither
- 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 typeEmpty
: 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 2int
, 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, return0
Full
: We have both digits so we can combine them
For example from the input
1abc2
we will extractFull (1, 2)
, and then usingdigits_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 currentdigits
value (initiallyEmpty
)str
: The current character (of typestring
)
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
ifstr
cannot be converted toint
Some
containingstr
converted toint
whenstr
represents a valid numbertype 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, returnEmpty
Empty
+Some
: No digits yet, and we found a new one, returnFull
containing the found digitn
Full
+None
: We already have some digits, and no new digit found, return sameFull
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 convertchr
(of typechar
) to astring
- 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.
Done! The final solution is the following:
(* 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.