Due date: Tuesday, September 10th, 11:59PM
In this course, we use OCaml for most programming assignments. This tutorial only covers the tiny sliver of OCaml you truly need to do the assignments. The course also uses tools and testing extensions that are not part of the standard OCaml distribution; this tutorial introduces them too.
The tutorial is divided into several sections. Each section introduces new OCaml features, provides some examples, and concludes with exercises that you should do. The solutions to all exercises are provided, but you should try to do the exercises before you click "show solution".
OCaml is a sophisticated programming language with several advanced features. We encourage you to explore OCaml on your own. There are several other tutorials and guides on the Ocaml Website. Real World OCaml is an excellent book that covers the language and programming pragmatics in depth.
Prerequisite: You must install the course software, which includes OCaml to do this tutorial. Do ask for help if you have trouble installing the software.
A simple OCaml program is a sequence of declarations, followed by
a main expression. You must use a double-semicolon (;;
)
to separate the declarations from the main expression. E.g.:
let x : int = 23
let y : int = 60
let sum : int = x + y
let message : string = "The sum is " ^ string_of_int sum ^ ".\n"
;;
print_string message
In this example, all the declarations are let declarations. (We introduce a few other kinds of declarations momentarily.) A let declaration states the name of the bound identifier, its type, and the bound expression. Unlike more complex languages, OCaml does not automatically convert between values of different types. For example, the code above explicitly applies string_of_int . Without this conversion, the OCaml compiler woudl fail with a type error:
This expression has type int but an expression was expected of type string.
Similarly, OCaml has no function or operator overloading. In many languages,
x + y
may mean "add two numbers" or "concatenate two strings".
In OCaml these two operations are distinct: x ^ y
means string
concatenation and x + y
means integer addition. Incidentally,
x +. y
means floating point addition.
In OCaml, we can declare functions using a variation of let declarations:
let increment (a : int) : int =
a + 1
let add_three (x : int) (y : int) (z : int) : int =
x + y + z
let make_message (n : int) : string =
"The sum is " ^ string_of_int n ^ ".\n"
;;
print_string (make_message (add_three 1 2 3))
In these declarations, each function argument has a name and type enclosed in parentheses. The final type in the declaration is the type of the function result.
Pay close attention to the syntax of function application in the main expression. A function application uses spaces to separate the function from each argument. The parentheses simply ensure that arguments are properly grouped. For example, suppose we missed the inner parentheses and wrote:
print_string (make_message add_three 1 2 3)
This expression applies make_message
to four arguments, but
it expects only one, so we have a type error. Similarly, suppose
we missed the outer parentheses:
print_string make_message (add_three 1 2 3)
This expression applies print_string
to two arguments, but
it expects only one, so we have another type error. When it doubt, use
parentheses for clarity.
In OCaml, let
does not introduce a recursive binding.
Therefore, to declare a recursive function, we must uselet rec
instead:
let rec factorial (n : int) : int =
if n = 0 then
1
else
n * factorial (n - 1)
;;
print_string ("factorial 5 = " ^ string_of_int (factorial 5) ^ "\n")
Printing strings in this manner is a very poor way to test code. You'll do better by writing test cases as follows:
TEST "testing factorial 5" =
factorial 5 = 120
TEST "testing factorial 8" =
factorial 8 = 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8
The TEST
notation is not standard OCaml, but a handy syntax extension
that we use extensively in this course. The course software will take
care of linking to the syntax extension for you.
For this course, you should use the cs691f
build tool to
compile programs and run tests. This program is a thin wrapper around the
OCaml's own build system that links to libraries and extensions
that we use in this course. If you want to look under the hood, the code
is
online
and in OCaml.
Place the declaration of factorial and its test cases, all of which
appear above, in a new file called tutorial.ml. Then, compile, run,
and test using the cs691f
tool:
$ cs691f compile tutorial $ cs691f run tutorial $ cs691f test tutorial
You should not get any errors.
Write a function, seconds_since_midnight h m s
, which returns
the number of seconds elapsed since midnight. Your function should have the
following type.
val seconds_since_midnight : int -> int -> int -> int
let seconds_since_midnight (hours : int) (mins : int) (secs : int) : int =
secs + 60 * (mins + 60 * hours)
TEST "midnight" = seconds_since_midnight 0 0 0 = 0
TEST "12:59:59PM" = seconds_since_midnight 23 59 59 = 86399
TEST "1:30AM" = seconds_since_midnight 1 30 0 = 3600 + 30 * 60
Write a function, fibonacci n
, which compute the nth
Fibonacci number. The function should have the following type:
val fibonacci : int -> int
let rec fibonacci n =
if n = 0 then
0
else if n = 1 then
1
else fibonacci (n - 1) + fibonacci (n - 2)
TEST "fib 2" =
fibonacci 2 = 1
TEST "fib 7" =
fibonacci 7 = 13
The previous section covered let declarations. This section introduces type declarations. Here is an example:
type point =
Point2D of int * int
This type declaration introduces a type called point
. It has one
constructor called Point2D
that takes two arguments, both
integers. Type names must begin with a lowercase letter and constructor names must
begin with an uppercase letter.
Given this declaration, we can use the constructor to create new points:
let origin : point = Point2D (0, 0)
let pt1 : point = Point2D (10, 20)
let pt2 : point = Point2D (-40, 50)
Above, the parentheses and commas are required: the constructor name must be followed by a comma-separated list of arguments enclosed in parentheses.
Define a type, time
, which holds the hour, minute,
and second as separate values.
type time = Time of int * int * int
Given a point value, we can use pattern matching to extract its components, as these functions do:
let double_point (p : point) : point =
match p with
Point2D (x, y) -> Point2D (2 * x, 2 * y)
let add_point (p1 : point) (p2 : point) : point =
match (p1, p2) with
(Point2D (x1, y1), Point2D (x2, y2)) -> Point2D (x1 + x2, y1 + y2)
let string_of_point (p : point) : string =
match p with
Point2D (x, y) -> "<" ^ string_of_int x ^ ", " ^ string_of_int y ^ ">"
In the following several exercises, you will use pattern matching to write functions that manipulate points. The OCaml manual has a section on integer arithmetic that will be helpful.
Write a function seconds_since_midnight2
with the
following type:
val seconds_since_midnight2 : time -> int
let seconds_since_midnight2 (t : time) : int =
match t with
| Time (hours, mins, secs) -> secs + 60 * (mins + 60 * hours)
Write a function seconds_to_time t
, which takes the seconds
elapsed since midnight as its argument and returns the coresponding time.
val seconds_to_time : int -> time
let seconds_to_time (n : int) : time =
let secs = n mod 60 in
let n = (n - secs) / 60 in
let mins = n mod 60 in
let n = (n - secs) / 60 in
let hours = n in
Time (hours, mins, secs)
TEST = seconds_to_time 0 = Time (0, 0, 0)
TEST = seconds_to_time 86399 = Time (23, 59, 59)
TEST = seconds_to_time 86400 = Time (24, 0, 0)
Write a function time_diff t1 t2
, which calculates
the number of seconds that have elapsed between t1
and
t2
:
val time_diff : time -> time -> int
let time_diff (t1 : time) (t2 : time) : int =
seconds_since_midnight2 t1 - seconds_since_midnight2 t2
TEST = let t = Time (1, 1, 1) in time_diff t t = 0
TEST = time_diff (Time (23, 59, 59)) (Time (0, 0, 0)) = 86399
Write a function tick t
, which increments t
by
one second and returns the new time:
val tick : time -> time
let tick (t : time) : time =
seconds_to_time (1 + seconds_since_midnight2 t)
TEST = tick (Time (0, 0, 59)) = Time (0, 1, 0)
TEST = tick (Time (1, 59, 59)) = Time (2, 0, 0)
TEST = tick (Time (23, 59, 59)) = Time (24, 0, 0)
For this part of the tutorial, we will work with a type declaration for lists of integers. There are two kinds of lists: the empty list and lists that have a single integer and a a reference to the rest of the list. We can specify the shape of lists using a type declaration with two constructors:
type intlist =
| Cons of int * intlist
| Empty
For example, here is a list of numbers from -1 through 4:
let from_minus_1_to_4 =
Cons (-1, Cons (0, Cons (1, Cons (2, Cons (3, (Cons (4, Empty)))))))
We can declare list processing functions using the same constructs we used to write date processing functions. However, since lists are recursively defined, most interesting list processing functions need to be recursive, too. For example, here is a function that calcululates the length of a list:
let rec length (lst : intlist) : int =
match lst with
| Empty -> 0
| Cons (first, rest) -> 1 + length rest
TEST = length Empty = 0
TEST = length from_minus_1_to_4 = 6
Write a function all_positive lst
, which returns true
if all the integers in lst
are positive.
val all_positive : intlist -> bool
let rec all_positive (lst : intlist) : bool =
match lst with
| Empty -> true
| Cons (n, rest) -> n > 0 && all_positive rest
TEST = all_positive (Cons (1, Empty)) = true
TEST = all_positive (Cons (-1, Empty)) = false
TEST = all_positive (Cons (1, Cons (-1, Empty))) = false
Write a function all_even lst
, which returns true
if all the integers in lst
are even numbers.
val all_even : intlist -> bool
let rec all_even (lst : intlist) : bool =
match lst with
| Empty -> true
| Cons (n, rest) -> n mod 2 = 0 && all_even rest
TEST = all_even (Cons (10, (Cons (2, Empty)))) = true
TEST = all_even (Cons (9, (Cons (1, Empty)))) = false
TEST = all_even Empty = true
Write the function is_sorted lst
to determine if the
integers in lst
are in sorted (ascending).
val is_sorted : intlist -> bool
Hint: You will need to write a recursive helper function.
let rec is_sorted_helper (prev : int) (lst : intlist) : bool =
match lst with
| Empty -> true
| Cons (n, rest) -> prev <= n && is_sorted_helper n rest
let is_sorted (lst : intlist) : bool =
match lst with
| Empty -> true
| Cons (n, rest) -> is_sorted_helper n rest
TEST = is_sorted (Cons (1, Cons (2, Cons (3, Empty)))) = true
TEST = is_sorted (Cons (1, Cons (1, Cons (3, Empty)))) = true
TEST = is_sorted (Cons (1, Cons (3, Cons (2, Empty)))) = false
Write the function insert_sorted n lst
, which inserts
n
into the sorted list lst
and preserves the
sort-order.
(* Assumes that [is_sorted lst] holds. *)
let rec insert_sorted (n : int) (lst : intlist) : intlist =
match lst with
| Empty -> Cons (n, Empty)
| Cons (m, rest) ->
(match n <= m with
| true -> Cons (n, Cons (m, rest))
| false -> Cons (m, insert_sorted n rest))
TEST = insert_sorted 5 Empty = Cons (5, Empty)
TEST =
insert_sorted 1 (Cons (0, Cons (2, Empty))) =
Cons (0, Cons (1, Cons (2, Empty)))
TEST = insert_sorted 10 (Cons (10, Empty)) = Cons (10, Cons (10, Empty))
Write the insertion_sort
function, using
insert_sorted
as a helper.
val insertion_sort : intlist -> intlist
let rec insertion_sort (lst : intlist) : intlist =
match lst with
| Empty -> Empty
| Cons (n, rest) -> insert_sorted n (insertion_sort rest)
TEST = insertion_sort (Cons (3, Cons (2, Cons (1, Empty)))) =
Cons (1, Cons (2, Cons (3, Empty)))
For this part of the tutorial, you will write an evaluator and pretty-printer for simple arithmetic expressions—the simplest of interpreters. Notably, this interpreter does not use any OCaml concept beyond those introduced above.
For the exercises below, use the following type declaration that represents arithmetic expressions.
type exp =
| Int of int
| Add of exp * exp
| Mul of exp * exp
Encode the following arithmetic expressions as exp
s:
let ex1 = Add (Int 10, Int 5)
let ex2 = Mul (Add (Int 2, Int 3), Int 5)
let ex3 = Mul ((Mul (Int 3, Int 0)), Mul (Int 3, Int 5))
Write the function eval e
, which reduces expressions to
integer values:
val eval : exp -> int
let rec eval (e : exp) : int = match e with
| Int n -> n
| Add (e1, e2) -> eval e1 + eval e2
| Mul (e1, e2) -> eval e1 * eval e2
TEST = eval (Add (Int 10, Int 5)) = 15
TEST = eval (Mul (Add (Int 2, Int 3), Int 5)) = (2 + 3) * 5
TEST = eval (Mul ((Mul (Int 3, Int 0)), Mul (Int 3, Int 5))) = 0
Write the function print e
, which returns a string
representing e
:
val print : exp -> string
The string should print arithmetic operators using infix notation and properly parenthesize expressions. For example, here are some tests:
TEST = print (Add (Int 10, Int 5)) = "(10 + 5)"
TEST = print (Mul (Add (Int 2, Int 3), Int 5)) = "((2 + 3) * 5)"
TEST = print (Mul ((Mul (Int 3, Int 0)), Mul (Int 3, Int 5))) = "((3 * 0) * (3 * 5))"
Use the ^ operator to concatenate strings and the string_of_int function.
let rec print (e : exp) : string = match e with
| Int n -> string_of_int n
| Add (e1, e2) -> "(" ^ print e1 ^ " + " ^ print e2 ^ ")"
| Mul (e1, e2) -> "(" ^ print e1 ^ " * " ^ print e2 ^ ")"
The print
function that you wrote above may be naive
about how it prints parentheses. For example, 1 + 2 * 3
is usually interpreted as 1 + (2 * 3)
because the *
operator +
operator.
Write the pretty_print e
function, which prints parentheses
only when necessary:
val pretty_print : exp -> string
pp cxt e
,
where e
is the expression to print and cxt
identifies
the shape of the immediately surrounding context.
type cxt =
| TopCxt (* top-level expression *)
| MulCxt (* [_ * x] or [x * _] *)
| AddCxt (* [_ + x] or [x + _] *)
let rec pp (enclosing : cxt) (e : exp) : string =
match e with
| Int n -> string_of_int n
| Add (e1, e2) ->
let inner = (pp AddCxt e1) ^ " + " ^ (pp AddCxt e2) in
(match enclosing with
| MulCxt -> "(" ^ inner ^ ")"
| _ -> inner)
| Mul (e1, e2) ->
(pp MulCxt e1) ^ " * " ^ (pp MulCxt e2)
let pretty_print (e : exp) : string = pp TopCxt e
TEST = pretty_print (Add (Int 10, Int 5)) = "10 + 5"
TEST = pretty_print (Mul (Add (Int 2, Int 3), Int 5)) = "(2 + 3) * 5"
TEST =
pretty_print (Mul ((Mul (Int 3, Int 0)), Mul (Int 3, Int 5))) = "3 * 0 * 3 * 5"