Damien Gonot
Home Blog Notes About

OCaml

Homepage / Notes / Computer Science / Programming Languages / OCaml

Garbage collection, static type-checking (with type inference), immutable programming, pattern matching.

Compilers

Opam

Opam is OCaml's package manager.

eval $(opam env) in new shells to activate Opam.

Dune

Dune is OCaml's build system.

REPL

Can just type ocaml to access a REPL. But an easier-to-use version is utop.

utop

Basic setup in ~/.ocamlinit to load packages every time utop is started:

#use "topfind";;
#thread;;

Language Features

Comments

(* this is a comment *)

Types

The basic types in OCaml are:

OCaml typeRange
'aAny type
int63-bit signed int on 64-bit processors, or 31-bit signed int on 32-bit processors
floatIEEE double-precision floating point, equivalent to C's double
boolA boolean, written either 'true' or 'false'
charAn 8-bit character
stringA string (sequence of 8 bit chars)

Custom Types

type colour = Red | Green | Blue
type colour = Red | Green | Blue
let l = [Red; Green; Green]
val l : colour list = [Red; Green; Green]
let l = [Red; Green; Yellow]
Line 1, characters 21-27:
1 | let l = [Red; Green; Yellow];;
                         ^^^^^^
Error: This variant expression is expected to have type colour
       There is no constructor Yellow within type colour

Pattern Matching on Types

type fruit = Banana | Strawberry | Apple;;

let get_colour = function
| Banana -> "yellow"
| Strawberry -> "red"
| Apple -> "green";;

get_colour Apple;;
green

Unboxed Types

https://www.janestreet.com/tech-talks/unboxed-types-for-ocaml/

Calculations

1 + 1
2

For floats, +. have to be used, as well as x. for the number to be added

1.5 +. 1.
2.5

Underscores (_) can be used to help read large numbers

2_000_000 / 20_000
100

Notice the difference between 1 / 3 and 1. /. 3.:

1 / 3
0
1. /. 3.
0.33333333333333331

Defining Variables

Variable names must start with a lowercase letter or an underscore.

let x = 3 + 4;;
7
let y = x + x;;
14

De-structuring let bindings

Can be used to define multiple variables at the same time:

let x, y = 8, 9;;
val x : int = 8
val y : int = 9

Chars

Chars use single-quotes:

'd';;
- : char = d

Strings

https://ocaml.org/api/String.html

Strings use double-quotes:

let name = "Damien";;
val name : string = "Damien"

List of Chars

Strings are essentially lists of characters

name.[0];;
- : char = 'D'
name.[1];;
- : char = 'a'

Length

String.length name;;
- : int = 6

Concatenation

^ is used to concatenate strings.

"Hello, " ^ name;;
"Hello, Damien"

Starts With

String.starts_with ~prefix:"Dam" name;;
- : bool = true
String.starts_with ~prefix:"ien" name;;
- : bool = false

Ends With

String.ends_with ~suffix:"ien" name;;
- : bool = true
String.ends_with ~suffix:"Dam" name;;
- : bool = false

Functions

let plus x y = x + y;;

plus 2 3;;
5

Example of partial application:

let plus_two = plus 2;;

plus_two 3;;
5
let square x = x * x;;

square 3
9
let ratio x y = Float.of_int x /. Float.of_int y;;

ratio 1 3;;
0.33333333333333331

Anonymous Functions

using stdlib

List.map (fun x -> x * 2) [1; 2; 3];;
- : int list = [2; 4; 6]

using Base from Jane Street

List.map [1; 2; 3] ~f:(fun x -> x*2);;
- : int list = [2; 4; 6]

Data Structures

Tuples

Ordered collection of values that can each be of a different type.

let tuple_a = (9, "nine");;
val tuple_a : int * string = (9, "nine")
let tuple_b = (9, "nine", 9.);;
val tuple_b : int * string * float = (9, "nine", 9.)

Values can be extracted from the tuple by using pattern matching:

let (x,y) = tuple_a;;
val x : int = 9
val y : string = "nine"
x + String.length y;;
- : Base.Int.t = 13

Lists

https://ocaml.org/api/List.html

Any number of (ordered) items of the same type.

let countries = ["United States"; "France"; "Canada"]
val countries : string list = ["United States"; "France"; "Canada"]

Mixing types is not possible in lists:

let numbers = [1;"two";3]
Line 1, characters 17-22:
1 | let numbers = [1;"two";3];;
                     ^^^^^
Error: This expression has type string but an expression was expected of type
         int
Semicolons vs Commas

Because commas are reserved to separate elements of tuples, using them in Lists returns a tuple inside a list:

["OCaml", "Python", "Ruby"];;
- : (string * string * string) list = [("OCaml", "Python", "Ruby")]

Even without parentheses, commas create a tuple:

1,2,3;;
- : int * int * int = (1, 2, 3)
Length

Getting the length of a list:

List.length countries;;
3
Nth
List.nth ["a"; "b"; "c"] 2;;
c
Mem

Short for "member of" list

List.mem "France" countries;;
- : bool = true
List.mem "China" countries;;
- : bool = false
Prepending

Prepending to a list:

"Germany" :: "Spain" :: countries;;
- : string list = ["Germany"; "Spain"; "United States"; "France"; "Canada"]

Note the initial list is unchanged:

countries;;
- : string list = ["United States"; "France"; "Canada"]
Concatenate Lists
[1; 2; 3] @ [4; 5; 6];;
- : int Base.List.t = [1; 2; 3; 4; 5; 6]
Pattern Matching on Lists

Compiler warns us that the code below is incomplete, because it doesn't support the case where countries is an empty list.

let favourite :: the_rest = countries;;
Line 1, characters 4-25:
1 | let favourite :: the_rest = countries;;;;
        ^^^^^^^^^^^^^^^^^^^^^
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
[]
val favourite : string = "United States"
val the_rest : string list = ["France"; "Canada"]

Using match instead:

let my_favourite_country countries =
  match countries with
  | first :: the_rest -> first
  | [] -> "Canada"
;;
val my_favourite_country : string list -> string = <fun>
my_favourite_country countries;;
United States
my_favourite_country [];;
Canada
Iter
List.iter print_endline ["a"; "b"; "c"];;
a
b
c
- : unit = ()
Map

map over list:

List.map String.length countries;;
- : int list = [13; 6; 6]

Using StdLabels:

open StdLabels;;

List.map ~f:String.length countries;;
- : int list = [13; 6; 6]
Map2

Called zip in most other languages?

List.map2 ( + ) [1; 2; 3] [4; 5; 6];;
- : int list = [5; 7; 9]
Find

Returns first element given predicate:

List.find (fun x -> x > 2) [1; 2; 3; 4; 5];;
3
Filter

Returns all element given predicate:

List.filter (fun x -> x > 2) [1; 2; 3; 4; 5];;
- : int list = [3; 4; 5]
Sort

Comparison feature compare can be used:

List.sort compare [3; 4; 1; 2; 4; 5];;
- : int list = [1; 2; 3; 4; 4; 5]

Fun.flip flips the arguments of a binary function, meaning x < y will become y < x:

List.sort (Fun.flip compare) [3; 4; 1; 2; 4; 5];;
- : int list = [5; 4; 4; 3; 2; 1]
Folds
Fold left
List.fold_left ( + ) 0 [1; 2; 3];;
6
List.fold_left ( - ) 0 [1; 2; 3];;
-6
Fold right

Accumulator is placed after the list:

List.fold_right ( + ) [1; 2; 3] 0;;
6
List.fold_right ( - ) [1; 2; 3] 0;;
2
Partition

If you also need elements which tested false:

List.partition (fun x -> x > 2) [1; 2; 3; 4; 5];;
- : int list * int list = ([3; 4; 5], [1; 2])
Recursive List Functions
let rec sum l =
  match l with
  | [] -> 0
  | hd :: tl -> hd + sum tl
;;
val sum : Base.Int.t list -> Base.Int.t = <fun>
sum [1;2;3];;
6
Association lists

Simplistic dictionary data structure:

let numbers = [(1, "one"); (2, "two"); (3, "three"); (4, "four"); (5, "five")];;
val numbers : (int * string) list =
  [(1, "one"); (2, "two"); (3, "three"); (4, "four"); (5, "five")]
Get value from key
List.assoc 3 numbers;;
- : string = "three"
Check that key exists
List.mem_assoc 3 numbers;;
- : bool = true
List.mem_assoc 6 numbers;;
- : bool = false
Split keys and values
List.split numbers;;
- : int list * string list =
([1; 2; 3; 4; 5], ["one"; "two"; "three"; "four"; "five"])
Combine keys and values to create an association list
List.combine [1; 2; 3; 4; 5] ["one"; "two"; "three"; "four"; "five"];;
- : (int * string) list =
[(1, "one"); (2, "two"); (3, "three"); (4, "four"); (5, "five")]

Records and Variants

type point2d = { x : float; y : float }
type point2d = { x : Base.float; y : Base.float; }
let p = { x = 3.; y = -4. };;
val p : point2d = {x = 3.; y = -4.}
let magnitude { x = x_pos; y = y_pos } =
  Float.sqrt (x_pos **. 2. +. y_pos **. 2.)
;;
val magnitude : point2d -> Base.Float.t = <fun>

Using field punning for a more terse definition:

let magnitude { x; y } = Float.sqrt (x **. 2. +. y **. 2.);;
val magnitude : point2d -> Base.Float.t = <fun>

You can re-use types as components of larger types:

type circle_desc  = { center: point2d; radius: float }
type circle_desc = { center : point2d; radius : Base.float; }

Maps

module Names = Map.Make(String);;
module Names :
  sig
    type key = String.t
    type 'a t = 'a Map.Make(String).t
    val empty : 'a t
    val is_empty : 'a t -> bool
    val mem : key -> 'a t -> bool
    val add : key -> 'a -> 'a t -> 'a t
    val update : key -> ('a option -> 'a option) -> 'a t -> 'a t
    val singleton : key -> 'a -> 'a t
    val remove : key -> 'a t -> 'a t
    val merge :
      (key -> 'a option -> 'b option -> 'c option) -> 'a t -> 'b t -> 'c t
    val union : (key -> 'a -> 'a -> 'a option) -> 'a t -> 'a t -> 'a t
    val compare : ('a -> 'a -> int) -> 'a t -> 'a t -> int
    val equal : ('a -> 'a -> bool) -> 'a t -> 'a t -> bool
    val iter : (key -> 'a -> unit) -> 'a t -> unit
    val fold : (key -> 'a -> 'b -> 'b) -> 'a t -> 'b -> 'b
    val for_all : (key -> 'a -> bool) -> 'a t -> bool
    val exists : (key -> 'a -> bool) -> 'a t -> bool
    val filter : (key -> 'a -> bool) -> 'a t -> 'a t
    val filter_map : (key -> 'a -> 'b option) -> 'a t -> 'b t
    val partition : (key -> 'a -> bool) -> 'a t -> 'a t * 'a t
    val cardinal : 'a t -> int
    val bindings : 'a t -> (key * 'a) list
    val min_binding : 'a t -> key * 'a
    val min_binding_opt : 'a t -> (key * 'a) option
    val max_binding : 'a t -> key * 'a
    val max_binding_opt : 'a t -> (key * 'a) option
    val choose : 'a t -> key * 'a
    val choose_opt : 'a t -> (key * 'a) option
    val split : key -> 'a t -> 'a t * 'a option * 'a t
    val find : key -> 'a t -> 'a
    val find_opt : key -> 'a t -> 'a option
    val find_first : (key -> bool) -> 'a t -> key * 'a
    val find_first_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val find_last : (key -> bool) -> 'a t -> key * 'a
    val find_last_opt : (key -> bool) -> 'a t -> (key * 'a) option
    val map : ('a -> 'b) -> 'a t -> 'b t
    val mapi : (key -> 'a -> 'b) -> 'a t -> 'b t
    val to_seq : 'a t -> (key * 'a) Seq.t
    val to_rev_seq : 'a t -> (key * 'a) Seq.t
    val to_seq_from : key -> 'a t -> (key * 'a) Seq.t
    val add_seq : (key * 'a) Seq.t -> 'a t -> 'a t
    val of_seq : (key * 'a) Seq.t -> 'a t
  end

Create an empty Names map n:

let n = Names.empty;;
val n : 'a Names.t = <abstr>

Add some data, by overwriting previous n:

let n = Names.add "Damien" "Gonot" n;;
val n : string Names.t = <abstr>

And more:

let n = Names.add "John" "Doe" n;;
let n = Names.add "Hercules" "Poirot" n;;
val n : string Names.t = <abstr>
let print_name first_name last_name =
  print_endline(first_name ^ " " ^ last_name);;
val print_name : string -> string -> unit = <fun>
Names.iter print_name n;;
Damien Gonot
Hercules Poirot
John Doe
- : unit = ()
Names.find "Damien" n;;
- : string = "Gonot"

Sets

module StringSet = Set.Make(String);;
module StringSet :
  sig
    type elt = String.t
    type t = Set.Make(String).t
    val empty : t
    val is_empty : t -> bool
    val mem : elt -> t -> bool
    val add : elt -> t -> t
    val singleton : elt -> t
    val remove : elt -> t -> t
    val union : t -> t -> t
    val inter : t -> t -> t
    val disjoint : t -> t -> bool
    val diff : t -> t -> t
    val compare : t -> t -> int
    val equal : t -> t -> bool
    val subset : t -> t -> bool
    val iter : (elt -> unit) -> t -> unit
    val map : (elt -> elt) -> t -> t
    val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
    val for_all : (elt -> bool) -> t -> bool
    val exists : (elt -> bool) -> t -> bool
    val filter : (elt -> bool) -> t -> t
    val filter_map : (elt -> elt option) -> t -> t
    val partition : (elt -> bool) -> t -> t * t
    val cardinal : t -> int
    val elements : t -> elt list
    val min_elt : t -> elt
    val min_elt_opt : t -> elt option
    val max_elt : t -> elt
    val max_elt_opt : t -> elt option
    val choose : t -> elt
    val choose_opt : t -> elt option
    val split : elt -> t -> t * bool * t
    val find : elt -> t -> elt
    val find_opt : elt -> t -> elt option
    val find_first : (elt -> bool) -> t -> elt
    val find_first_opt : (elt -> bool) -> t -> elt option
    val find_last : (elt -> bool) -> t -> elt
    val find_last_opt : (elt -> bool) -> t -> elt option
    val of_list : elt list -> t
    val to_seq_from : elt -> t -> elt Seq.t
    val to_seq : t -> elt Seq.t
    val to_rev_seq : t -> elt Seq.t
    val add_seq : elt Seq.t -> t -> t
    val of_seq : elt Seq.t -> t
  end
let s = StringSet.singleton "hello";;
val s : StringSet.t = <abstr>
let s = List.fold_right StringSet.add ["world"; "stranger"] s;;
val s : StringSet.t = <abstr>
StringSet.iter (fun str -> print_endline str) s;;
hello
stranger
world
- : unit = ()
StringSet.mem "stranger" s;
- : bool = true

Hash Tables

1000 is the initial size of the hash table. Hash tables can grow further is size has been underestimated.

let my_hash = Hashtbl.create 1000;;
val my_hash : ('_weak1, '_weak2) Hashtbl.t = <abstr>

'_weak1 is the type for the key and '_weak2 the type for the value

There are no concrete types yet. The underscore indicates that once the key and value types will be chosen, they'll be fixed.

Hash tables are updated in-place, adding a new member doesn't return a new hash table like maps do.

Hashtbl.add my_hash "h" "hello";;
Hashtbl.add my_hash "h" "harbour";;
Hashtbl.add my_hash "w" "world";;
Hashtbl.add my_hash "w" "win";;
Hashtbl.add my_hash "w" "wonderful";;
- : unit = ()

Types are fixed now:

my_hash;;
- : (string, string) Hashtbl.t = <abstr>
Find

Hashtbl.find returns the last added element:

Hashtbl.find my_hash "h";;
- : string = "harbour"
Find all

To find all elements:

Hashtbl.find_all my_hash "w";;
- : string list = ["wonderful"; "win"; "world"]
Replace

Instead of using Hashtbl.add, we can use Hashtbl.replace if we only want one value per key:

Hashtbl.replace my_hash "t" "try";;
Hashtbl.replace my_hash "t" "test";;
Hashtbl.find_all my_hash "t";;
- : string list = ["test"]
Hashtbl.remove my_hash "t";;
Hashtbl.find my_hash "t";;
Exception: Not_found.

Scope

let z = 7 in
z + z
;;
14

The scope of the let binding is terminated by the ;;, value of z is no longer available outside that scope:

z;;
Line 1, characters 0-1:
1 | z;;;;
    ^
Error: Unbound value z

Those can be nested:

let x = 7 in
let y = x * x in
x + y
;;
56

If expressions

let max a b =
  if a > b then a else b;;
val max : 'a -> 'a -> 'a = <fun>
max 9 10;;
10

Options

Used to express that a value might or might not be present.

let divide x y =
  if y = 0 then None else Some (x / y)
;;
val divide : int -> int -> int option = <fun>

Values can't be null in OCaml. Missing values are explicit. If you want to allow some data to be absent, you have to use Options.

Pipes

|> pipes work!

[1; 2; 3] |> List.map (fun x -> x * 2)
- : int list = [2; 4; 6]

Multiple pipes!

[1; 2; 3] |> List.map (fun x -> x * 2) |> List.fold_left (+) 0
- : int = 12

@@ is like a reverse pipe

List.map (fun x -> x * 2) @@ [1; 2; 3]
- : int list = [2; 4; 6]

Imperative Programming

OCaml is mostly pure and functional. Almost all data structures are immutable. But OCaml does support imperative programming.

Arrays

let numbers = [| 1; 2; 3; 4; 5 |];;
val numbers : int array = [|1; 2; 3; 4; 5|]

.(i) is used to access an element of an array by index i. The <- syntax is used for modification.

numbers.(2) <- 4;;
- : unit = ()

Because the elements of the array are counted starting from zero, .i(2) is the third element.

numbers;;
- : int array = [|1; 2; 4; 4; 5|]

Refs

Basic
let x = { contents = 0 };;
val x : int ref = {contents = 0}
x.contents <- x.contents + 1;;
- : unit = ()
x;;
- : int ref = {contents = 1}
More Terse Syntax
let x = ref 0;;
val x : int ref = {contents = 0}
!x;;
- : int = 0
x := !x + 1;;
- : unit = ()
!x;;
- : int = 1
Real-life Example
let sum list =
  let sum = ref 0 in
  List.iter (fun x -> sum := !sum + x) list;
  !sum
;;
val sum : int list -> int = <fun>
sum [5; 5; 5];;
- : int = 15

For loops

for i = 1 to 5 do
  print_endline (string_of_int i)
done
1
2
3
4
5
- : unit = ()
for i = 5 downto 1 do
  print_endline (string_of_int i)
done
5
4
3
2
1
- : unit = ()

While loops

Have to use refs in order to be able to quit the loop

let quit_loop = ref false in
  while not !quit_loop do
    print_endline "this will print once";
    quit_loop := true
  done
this will print once
- : unit = ()

Modules

Module names always start with an uppercase letter.

Local Imports

let ratio x y =
  let open Float.O in
  of_int x / of_int y
;;
val ratio : int -> int -> Base.Float.t = <fun>

More concise syntax:

let ratio x y =
  Float.O.(of_int x / of_int y)
;;
val ratio : int -> int -> Base.Float.t = <fun>

File manipulation

https://ocaml.org/learn/tutorials/file_manipulation.html

Packages

Base

https://ocaml.janestreet.com/ocaml-core/v0.12/doc/base/index.html

An addition to the standard library developed by Jane Street.

OCaml for the Web

https://esy.sh/en/

Dream

https://aantron.github.io/dream/

Tidy, feature-complete Web framework

Dream CLI

https://github.com/tmattio/dream-cli

YOCaml

https://github.com/xhtmlboi/yocaml

YOCaml is a static site generator, mostly written in OCaml

Bonsai

https://github.com/janestreet/bonsai

Bonsai is a library for building interactive browser-based UI.

Caqti

https://github.com/paurkedal/ocaml-caqti

Cooperative-threaded access to relational data

kind of like Python's SQLAlchemy or Java's JDBC

Resources

https://medium.com/@bobbypriambodo/interfacing-ocaml-and-postgresql-with-caqti-a92515bdaa11

https://ceramichacker.com/blog/28-2x-backend-webdev-w-dream-and-caqti

Petrol

https://github.com/Gopiandcode/petrol

Petrol is a Free software library that provides a high-level OCaml API for interacting with SQL databases. The aim of this interface is to provide a type-safe API to allow developers to define their SQL tables and queries directly in OCaml, thereby avoiding the impedence mismatch and fragility that comes with having to directly write SQL code, as is typical in a normal Caqti-based project.

irmin

https://irmin.org/

A distributed database built on the same principles as Git

Opium

https://github.com/rgrinberg/opium

Sinatra like web toolkit for OCaml

MintTea

https://github.com/leostera/minttea

A fun little TUI framework for OCaml

Tools

Spin

https://github.com/tmattio/spin

OCaml project generator.

Melange

https://github.com/melange-re/melange

A mixture of tooling combined to produce JavaScript from OCaml & Reason

Resources

OCaml docs

https://ocaml.org/learn/

The OCaml API

https://v2.ocaml.org/api/index.html

The OCaml system

https://v2.ocaml.org/releases/4.14/htmlman/index.html

Cornell CS3110

Real World OCaml

https://dev.realworldocaml.org/

Heavily uses Jane Street packages

OCaml from the Very Beginning

https://johnwhitington.net/ocamlfromtheverybeginning/index.html

What I wish I knew when learning OCaml

https://baturin.org/docs/ocaml-faq/

OCaml 5 tutorial

https://github.com/kayceesrk/ocaml5-tutorial/

OCaml Exercises

https://ocaml.org/problems

Sherlocode

https://sherlocode.com/

Realtime grep for OCaml sources available on opam