CS691F Programming Languages

| Introduction | Schedule | Software | OCaml Standard Library | Course Libraries |

OCaml Tutorial

Due date: Tuesday, September 10th, 11:59PM


Introduction

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.

Basic Programming and Testing

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.

Compiling and Testing

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.

Exercise 1

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.

Exercise 2

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
Show solution

Exercise 3

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
Show solution

Declaring New Types

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.

Exercise 4

Define a type, time, which holds the hour, minute, and second as separate values.

type time = Time of int * int * int
Show solution

Pattern Matching

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.

Exercise 5

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)
Show solution

Exercise 6

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)
Show solution

Exercise 7

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
Show solution

Exercise 8

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)
Show solution

Recursive Data Structures: Lists

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

Exercise 9

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
Show solution

Exercise 10

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
Show solution

Exercise 11

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
Show solution

Exercise 12

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))
Show solution

Exercise 13

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)))
Show solution

Evaluating Arithmetic Expressions

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

Exercise 14

Encode the following arithmetic expressions as exps:

  1. 10 + 5
  2. (2 + 3) * 5
  3. 3 * 0 * 3 * 5

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))
Show solution

Exercise 15

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
Show solution

Exercise 16

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 ^ ")"
Show solution

Exercise 17

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 binds tighter than the + operator. Write the pretty_print e function, which prints parentheses only when necessary:

val pretty_print : exp -> string

Hint: You will need a helper function, 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"
Show solution