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, andbubblewrapare 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 tonpmfor 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) yIt 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 environmentSetting 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-releaseThis 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 🤩
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
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.mlThis is your first program in OCaml: "Hello, World!".
Create a project using dune
dune is the standard build system for OCaml.
duneallows 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
We can then compile the project running the build command:
dune buildFinally, we can execute the program by running the exec command:
dune exec exampleThis 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 buildcommand- Run the
dune exec ...command
Code formatting
We need to create a new .ocamlformat file to enable auto-formatting:
profile = janestreet
version = 0.26.1Now we can execute the formatting command:
dune fmt # or dune build @fmtFinal 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 asprofilelib/dune/dune-project: Modules configurations,namedefines the export to use insidebin(in the examplename aocmeans that you useAocas module insidebin)
(library
(name aoc))bin/dune/dune-project: Execution configurationpublic_namedefines the command to run withexec(in the examplepublic_name aocmeans callingdune exec aoc)namereferences the name of the entry file to execute (in the examplename mainmeans executing themain.mlfile)librariesreferences thenameassigned in thelib/dune/dune-projectfile
(executable
(public_name aoc)
(name main)
(libraries aoc))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 # ExecuteThere is more 🤩
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
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 + bOCaml is very smart in inferring types for you, no need to write them manually.
In the
sumfunction OCaml knows thataandbareintbecause we use the+operator (which works oninttypes).
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
OptionandEither - 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 * intdigits: 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 * intrepresents 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
digitsallows 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) + lastNumThe 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, return0Full: We have both digits so we can combine them
For example from the input
1abc2we 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) + lastNumCollecting 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 currentdigitsvalue (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
optionwill be:
Noneifstrcannot be converted tointSomecontainingstrconverted tointwhenstrrepresents a valid numbertype option = | None | Some intYou can learn more about the
Optiontype 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, returnEmptyEmpty+Some: No digits yet, and we found a new one, returnFullcontaining the found digitnFull+None: We already have some digits, and no new digit found, return sameFullFull+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 sourcefun 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.escapedis used to convertchr(of typechar) to astring- We pass
Emptyas 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_sumprogram 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_sumThis 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.
