This section covers some basics first, and then gives a structured introduction of programming in OCaml
OCaml files are written with a .ml
extension. An OCaml file
consists of
open
statements for including other
modulesFor example:
open Printf
let message = "Hello world";;
(printf "%s\n" message)
The first line includes the built-in library for printing, which provides
functions similar to fprintf
and printf
from
stdlib
in C. The next two lines define a constant named
message
, and then call the printf
function with a
format string (where %s
means “format as string”), and the
constant message
we defined on the line before.
Put this in a file called hello.ml
and run it with:
$ ocaml hello.ml
Hello world
Most of our programs will be more complex than this, and much of the infrastructure for the “main” function will be provided, but it’s useful to see the simplest case first.
Here is an example of a function definition in OCaml:
let max (n : int) (m : int) : int =
if n > m then n else m;;
It uses the keyword let
followed by the name of the function
(here, max
). Next comes the parameter list. Each parameter is
enclosed in parentheses, and has the parameter’s name, then a colon, then its
type. So n
and m
are the parameters of
max
, and they are both type int
. The final colon
followed by int
describes the return type of the function. Then
there is an =
sign, followed by the function body.
This declaration is similar to the following in C++ or Java:
int max(int n, int m) {
if(n > m) { return n; }
else { return m; }
}
One notable difference is that the OCaml function does not have any
return
statements. We’ll talk about how to think about the
“return” value of a function without return
statements next. It’s
also important to note that the declaration in OCaml ends in ;;
.
This is a required syntactic convention for all top-level declarations in
OCaml.
The syntax for function calls in OCaml is different than you may be used to. Instead of writing
some_function(arg1, arg2, ...)
// for example
max(4, 5)
as we would in C++ or Python, in OCaml we write
(some_function arg1 arg2 ...)
// for example
(max 4 5)
(The surrounding parentheses can be omitted in some cases, but it’s always safe to include them to be clear.)
There is a useful model to think about what happens when we call a function
in OCaml. Rather than thinking about a call to max
creating a new
stack frame, let’s think about what happens if we substitute the
provided argument values for the parameters in the body of max
,
and continue by evaluating the function body. This notion of substitution is
“just” a useful model; everything you know about stacks and memory diagrams is
still true. But substitution is a very helpful model for reasoning in the style
of programming we’ll do.
So, for example, the call to max
below takes a step to
the substituted form:
(max 4 5)
=> if 4 > 5 then 4 else 5
Then we can think about how the if
expression takes steps.
First, it evaluates the conditional part, and based on that value being
true
or false
, it evaluates one or the other
branch:
if 4 > 5 then 4 else 5
=> if false then 4 else 5
=> 5
From this sequence of steps, we say that (max 4 5)
evaluates to 5
. This gives us a way to think about
evaluation that does not require a notion of a return
statement.
OCaml distinguishes between functions that can contain recursive calls and
functions that cannot. We saw the latter kind above in max
which
simply used the let
keyword. We can define a recursive function
by using let rec
:
let rec factorial (n : int) : int =
if n <= 1 then 1
else n * (factorial (n - 1));;
The substitution-based rules are a little more interesting when thinking about
evaluating a call to factorial
:
(factorial 3)
=> (if 3 <= 1 then 1 else 3 * (factorial (3 - 1)))
=> (if false then 1 else 3 * (factorial (3 - 1)))
=> (3 * (factorial (3 - 1)))
=> (3 * (factorial 2))
=> (3 * (if 2 <= 1 then 1 else 2 * (factorial (2 - 1)))
...
=> (3 * (2 * (factorial (2 - 1))))
...
=> (3 * (2 * 1))
=> 6
Here, we can see the chained multiplications “stack up” during the recursive calls. Writing this in a substitution-based style makes it easy to track where the return values of function calls end up.
This section shows how to create new datatypes in OCaml, and program with them.
type
Let us start with a binary tree datatype. We know we will need to represent
a binary tree node somehow, which has a value and two children. For now, let
us say the value has to be a string. In OCaml, we can define such a node using
the keyword type
:
type btnode =
| Leaf
| Node of string * btnode * btnode
Translated into English, this reads:
A binary tree node is either a leaf of the tree, which has no fields, or a node, which has three fields: a string, a binary tree node, and another binary tree node.
This defines what we call constructors for Leaf
and Node
,
which we can use to construct trees. Here are a few examples of trees and
their corresponding btnode
value:
"a" Node("a",
/ \ Node("b", Leaf, Leaf), Node("c", Leaf, Leaf))
"b" "c"
"a" Node("a",
/ Node("b", Leaf, Leaf), Leaf)
"b"
"a" Node("a",
/ Node("b",
"b" Leaf, Node("c", Leaf, Leaf)), Leaf)
\
"c"
Each position with no child corresponds to a Leaf
, and the
others correspond to uses of Node
. We call Leaf
and
Node
variants of the btnode
type. (
A Leaf
is used here where you may have seen NULL
or
null
in a C++ or Java implementation of a binary tree.)
match
The next question is how to work with these values. For example, how can we
construct an in-order concatenation of the strings in a btnode
as
we have defined it? That is, how do we fill in this function:
let rec inorder_str (btn : btnode) : string =
...
The next feature we need to introduce is match
, which allows us
to examine which variant of a type a particular value has, and extract the
values of its fields. Here are some examples:
let m1 = match Leaf with
| Leaf -> true
| Node(s, left, right) -> false;;
(* m1 is true *)
let m2 = match Leaf with
| Leaf -> 44
| Node(s, left, right) -> 99;;
(* m2 is 44 *)
let m3 = match Node("a", Leaf, Leaf) with
| Leaf -> "z"
| Node(s, left, right) -> s;;
(* m3 is "a" *)
let m4 = match Node("a", Node("b", Leaf, Leaf), Leaf) with
| Leaf -> "z"
| Node(s, left, right) ->
match left with
| Leaf -> "y"
| Node(s2, left2, right2) -> s2;;
(* m4 is "b" *)
From these examples, we can see how match
must work. It
inspects the value after the match
keyword, and selects the branch
that corresponds to the variant of that value. Then it extracts the fields
from the value, and substitutes them for the names given in the branch. Let us
use the m4
example to make that concrete:
match Node("a", Node("b", Leaf, Leaf), Leaf) with
| Leaf -> "z"
| Node(s, left, right) ->
match left with
| Leaf -> "y"
| Node(s2, left2, right2) -> s2
(* substitute Node("b", Leaf, Leaf) for left *)
=> match Node("b", Leaf, Leaf) with
| Leaf -> "y"
| Node(s2, left2, right2) -> s2
(* substitute "b" for s2 *)
=> "b"
With match
available, we can now fill in the body for
inorder_str
. We can start by writing out a skeleton of the match
structure for a btnode
, as most functions over
btnode
s will need to match
on the node to decide what
to do.
let rec inorder_str (bt : btnode) : string =
match bt with
| Leaf -> ...
| Node(s, left, right) -> ...
Now we can ask what the inorder traversal should yield in the case of a leaf
of the tree (or an empty tree altogether). In this case, that ought to be an
empty string. So the Leaf
case should be filled in with
""
. How about for Node
s? We know an inorder
traversal should have the elements to the left in order, then the current node,
then the elements to the right. We can get the elements to either side via a
recursive call, and then we just need one more piece of information, which is
that ^
is the operator for concatenating strings in OCaml:
let rec inorder_str (bt : btnode) : string =
match bt with
| Leaf -> ""
| Node(s, left, right) ->
(inorder_str left) ^ s ^ (inorder_str right)
Since we have seen binary trees, it’s natural to think about a similar definition for the nodes of a linked list. One OCaml datatype we could write is:
type llist =
| Empty
| Link of string * llist
That is, a list is either Empty
(the end of the list), or a
Link
of a string onto another list. Of course, this would require
that we write additional datatype declarations for lists of numbers, lists of
booleans, lists of binary trees, and so on, if we needed those shapes of data.
The natural solution is to make the datatype generic over the kind of data it
uses. OCaml lets us do this by defining datatypes with type variables
that can be filled with any type. Type variables are written with a leading
'
character:
type 'a llist =
| Empty
| Link of 'a * 'a llist
The types of the fields in Link
have changed with this addition. The
first field can now hold a value of the list’s type, and the second must hold
a llist
that contains elements of that type as well. That is, this
describes a homogeneous linked list, where all elements will have the
same type.
Let us say we want to write a function sum
that takes a
llist
of numbers and adds them up. We now need to describe its
type in terms of the contents, which will be an int
:
let rec sum (l : int llist) : int =
match l with
| Empty -> 0
| Link(first, rest) -> first + (sum rest)
When we construct llist
s, we do not need to provide any extra type
information – OCaml figures it out for us. For example, we can write:
let l1 = Link(1, Empty);;
let l2 = Link("a", Empty);;
Here, l1
will have type int llist
, and l2
will have type
string llist
.
It turns out that our definition of llist
above is important
enough that a version of it is built into OCaml, just with slightly different
names and syntax. The built-in equivalent of Empty
is written
[]
, and Link(first, rest)
is written
first::rest
. The syntax [a;b;c]
is shorthand for
a::b::c::[]
. The type of built-in lists is 'a list
,
which can be specialized for any list contents. For example, we could rewrite
sum
above as:
let rec sum2 (l : int list) : int =
match l with
| [] -> 0
| first::rest -> first + (sum2 rest)
And we could test it by creating tests with t_int
:
(* these would go in the suite list *)
t_int "sum2_empty" (sum2 []) 0;
t_int "sum2_single" (sum2 [5]) 5;
t_int "sum2_longer" (sum2 [3; 4; 5]);
t_int "sum2_longer2" (sum2 (3::4::5::[]));
Note that the last two tests mean the same thing; they are just different ways of writing the same list containing 3, 4, and 5.
Since lists are quite a fundamental structure, we will end up using them frequently; handy functions to use with lists are here.
There are many times in programs where we wish to return more than one value. For example, when returning a pair of key and value from a hash-table data structure, when returning an average and its standard deviation, or when representing a two (or three)-dimensional point, to name a few.
OCaml has a built-in way of handling these cases called tuples
(many other languages do as well). To create a tuple, we enclose two or more
values in parentheses:
let tup = (1, "a", []);;
To access the values in a tuple, we can use a special kind of let binding, where we give names to the positions of a tuple value:
let tup = (1, "a", []);;
let (one, a, empty_list) = tup;
(*
one is 1
a is "a"
empty_list is []
*)
Since pairs—tuples of exactly two elements—are quite common, there are also
two built-in functions, fst
and snd
, that get the
first and second component of a two-element tuple, respectively.
The type
of a tuple is written with *
characters
separating the components’ types, and surrounded by parentheses.
let increment_snd (t : (string * int)) : (string * int) =
(fst t, 1 + (snd t));;
(increment_snd ("a", 5)) (* returns the pair ("a", 6) *)
option
A common way of handling failure is raising exceptions with
failwith
. This works well when an operation is truly nonsensical.
However, it forces programs to use a different class of features— exceptions
and exception handlers—to handle failing behaviors. Sometimes, the failure of
an operation is a reasonable outcome, and having a way to report a failure, or
the absence of an answer, with a normal value rather than an exception is quite
useful.
Consider the problem of finding and returning the first element in a list that matches a particular predicate:
let rec find (l : 'a list) (pred : 'a -> bool) : 'a =
match l with
| [] -> failwith "Not found"
| x::xs -> if pred x then x else find xs pred;;
(find [1;2;3] (fun n -> n > 4);; (* raises an error *)
(find [1;2;3] (fun n -> n > 2);; (* returns 3 *)
When the element is not found, we cannot return a value of type
'a
, because the algorithm hasn’t found one. It seems we have to
throw an error, as there is nothing left for us to do. This certainly limits
the utility of find
, which now needs a companion
contains
if is to be useful on lists that are not already known to
have a matching element.
It would be convenient if we had a value that represented that there is
no appropriate value to return in the empty case. Similarly,
it would be useful to have the counterpart, a representation of being able to
provide some appropriate value. OCaml provides just such a
datatype, called option
, which is built-in. If we wrote the
definition ourselves, it would look like:
type 'a option =
| None
| Some of 'a
That is, an option
is either None
, which we can
use to indicate failure or the lack of an appropriate value, or
Some
, which contains a single field that is a value of the
option’s type. To write find
using option, we would write:
let rec find_opt (l : 'a list) (pred : 'a -> bool) : 'a option =
match l with
| [] -> None
| x::xs -> if pred x then Some(x) else find_opt xs pred;;
(find_opt [1;2;3] (fun n -> n > 4);; (* returns None *)
(find_opt [1;2;3] (fun n -> n > 2);; (* returns Some(3) *)
Now a program that calls find
, rather than using an exception
handler to manage the not found case, can simply match
on the
option
that is returned to decide what to do.
Note that option
s aren’t always better than exceptions, as
sometimes it’s difficult for the caller to know what to do when
None
is returned. But in many cases, when “failure” is something
that the caller can reasonably react to, returning an option
is a
much more natural choice.