OCaml
Homepage / Notes / Computer Science / Programming Languages / OCaml
Garbage collection, static type-checking (with type inference), immutable programming, pattern matching.
Compilers
ocamlc
bytecode compilerocamlopt
native code compiler
Opam
Opam is OCaml's package manager.
eval $(opam env)
in new shells to activate Opam.
opam init
to initialize~/.opam
opam search {query}
to search for package{query}
.opam show {package}
to display information about{package}
.opam install x
to installx
package.opam update
to update package list.opam upgrade
to upgrade installed packages to their latest version.
Dune
Dune is OCaml's build system.
dune init proj helloworld
to inithelloworld
project.dune build
to build.dune exec helloworld
to build & run.
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 type | Range |
---|---|
'a | Any type |
int | 63-bit signed int on 64-bit processors, or 31-bit signed int on 32-bit processors |
float | IEEE double-precision floating point, equivalent to C's double |
bool | A boolean, written either 'true' or 'false' |
char | An 8-bit character |
string | A 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
"yellow"
| Banana -> "red"
| Strawberry -> "green";;
| Apple ->
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
0];; name.[
- : char = 'D'
1];; name.[
- : 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;;
2 3;; plus
5
Example of partial application:
let plus_two = plus 2;;
3;; plus_two
5
let square x = x * x;;
3 square
9
let ratio x y = Float.of_int x /. Float.of_int y;;
1 3;; ratio
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"
String.length y;; x +
- : 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>
1;2;3];; sum [
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 } =
sqrt (x_pos **. 2. +. y_pos **. 2.)
Float. ;;
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 = ()
"Damien" n;; Names.find
- : 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>
fun str -> print_endline str) s;; StringSet.iter (
hello
stranger
world
- : unit = ()
"stranger" s; StringSet.mem
- : 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.
2) <- 4;; numbers.(
- : 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}
1;; x.contents <- x.contents +
- : unit = ()
x;;
- : int ref = {contents = 1}
More Terse Syntax
let x = ref 0;;
val x : int ref = {contents = 0}
!x;;
- : int = 0
1;; x := !x +
- : 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>
5; 5; 5];; sum [
- : 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";
true
quit_loop := 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
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
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
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
- https://www.cs.cornell.edu/courses/cs3110/2019sp/textbook/
- https://cs3110.github.io/textbook/cover.html
- https://youtube.com/playlist?list=PLre5AT9JnKShBOPeuiD9b-I4XROIJhkIU
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
Sherlocode
Realtime grep for OCaml sources available on opam