Skip to content

Lecture 1.3: Getting Started with OCaml; Calculator Expressions

Overview of Syllabus

The syllabus can be found here.

Setting up OCaml

Installation instructions are here. Alternative instructions are here.

For Mac and Linux:

  1. Run bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh)" in a terminal. Press enter when it asks you where to install it, and enter your password when it asks you to.
  2. Run opam init; when it asks you how to modify zsh, just press enter.
  3. Run opam install ocaml-lsp-server odoc ocamlformat utop. If it says that no switch exists, first run opam switch create cs2800 ocaml-base-compiler.5.3.0.
  4. Try running utop. If that doesn't work, try first running eval $(opam env). If it works after running eval $(opam env), run this command so that you don't have to again: echo 'eval $(opam env)' >> ~/.zshrc.
  5. (Recommended) Install VSCode, search for "OCaml", and install the official OCaml Platform extension.

For Windows:

  1. Install Windows Subsystem for Linux. Open Powershell in administrator mode (right-click on PowerShell and select "Run and administrator"). Then, enter the following command: wsl --install
  2. Once WSL finishes installing, you can open an Ubuntu terminal via the Start menu.
  3. Then, follow the instructions for Mac/Linux above. When you install VSCode, you also need to

First Steps: Arithmetic Expressions

There are two ways we will run OCaml: via utop, OCaml's REPL (Read-Eval-Print-Loop), which allows you to type in commands interactively; and via ocamlc, which compiles OCaml to lower-level code that can be executed. We will begin with utop.

In utop, you can write and evaluate OCaml expressions:

utop # 2 + 2;;
- : int = 4

The top line is our input 2+2;;, while the bottom line shows the result. We must end commands to utop with ;;, because newlines are ignored in OCaml:

utop # 2 +
2;;
- : int = 4

The bottom line shows utop's response. This response contains two things: a type (here, int), and a resulting value (unsurprisingly, we get 4). The type int is for integers: i.e., whole numbers. OCaml supports addition, multiplication, and integer division through the usual operators:

utop # 2 * 3 + 1;;
- : int = 7

Integer division in OCaml (like many other languages) will round down to the nearest integer. You can get the remainder by using the infix operator mod:

utop # 7 / 3;;
- : int = 2

utop # 7 mod 3;;
- : int = 1

utop # (7 / 3) * 3 + (7 mod 3);;
- : int = 7

Exponentiation

Exponentiation in OCaml is not built-in, but we will later be able to define it.

To quit utop, type CTRL-D, or type #quit;;.

Floating-Point Numbers

If you want to not only use whole numbers, you can use the float type in OCaml, which denotes floating-point numbers. You can think of these as numbers with decimals.

utop # 3.2;;
- : float = 3.2

Note that the type for 3.2 is float, not int. Arithmetic in OCaml is strongly typed, which means that arithmetic operators over int do not work over float. Let's try it out:

utop # 3.2 + 5;;
Error: The constant 3.2 has type float but an expression was expected of type int

OCaml gives us a type error because 3.2 is a float, but + expects an int on both sides. To fix this, we use floating-point variations of each operator. For addition, that is +.. Let's try that out:

utop # 3.2 +. 5;;
Error: The constant 5 has type int but an expression was expected of type float
    Hint: Did you mean 5.?
Oops! +. expects floats on both sides, but 5 has type int. The error message we get tells us that we should instead use 5., which is the floating-point literal for five:
utop # 3.2 +. 5.;;
- : float = 8.2

utop # 3.2 *. 5.;;
- : float = 16.

utop # 5.3 /. 5.;;
- : float = 1.06

Booleans

The two booleans in OCaml are written true and false. The operators between them are || (for OR), && (for AND) and not:

utop # true;;
- : bool = true

utop # true || false;;
- : bool = true

utop # true && false;;
- : bool = false

utop # not true;;
- : bool = false

utop # not false || true;;
- : bool = true
Note in the last line, not false || true gets interpreted as (not false) || true. Instead, we could have inserted parentheses to force the other interpretation:
utop # not (false || true);;
- : bool = false

Booleans can be inspected using if .. then .. else ..:

utop # if false then 2 else 3;;
- : int = 3

Strings

Strings can be created by using double quotes:

utop # "hi";;
- : string = "hi"

Strings can be concatenated together (put side by side) by using the ^ operator (don't confuse it with exponentiation!)

utop # "hi " ^ "there";;
- : string = "hi there"

Let expressions

Expressions can locally define variables using let x = .. in ..:

utop # let x = 8 in x / 2 ;;
- : int = 4

These can be arbitrarily nested and sequenced:

utop # let a = 1 in let b = 2 in a + b;;
- : int = 3

utop # let a = 2 + 2 in 3 * a ;;
- : int = 12

Which can be written in a cleaner style by using newlines:

utop # let a = 1 in
       let b = 2 in
       a + b;;
- : int = 3

Let expressions can also be nested inside on the left-hand side of another let expression:

utop # let x =
   let a = 1 in a + 1
   in
   x * 2;;
- : int = 4

utop # let x = 
    (let a = 1 in a) * (let x = 2 in x + 1) in 
    100 / x
    ;;
- : int = 33

In utop, you can define variables without giving a right-hand side in ..:

utop # let x = 3;;
val x : int = 3

utop # x + 1;;
- : int = 4

The binding of x will be available for the rest of the utop session. You can update the binding by re-defining it:

utop # let x = 3;;
val x : int = 3

utop # let x = 2;;
val x : int = 2

utop # x;;
- : int = 2

You can even make reference to the old version of x while defining the new one:

utop # let x = 3;;
val x : int = 3

utop # let x = x + 1;;
val x : int = 4
However, you should not think of this as mutating or modifying x! Instead, this is re-defining a new value of x based on the old one.

Equality

Integers can be tested for equality using =:

utop # let x = 2 + 2 in 
       let y = 2 * 2 in 
       if x = y then 3 else 4;;
- : int = 3

We can use = for Booleans as well:

utop # false = true;;
- : bool = false

The = operator will be useful for other types too; we will revisit this later.

Underscore variables

It will be useful to know later that you can ignore the result of a let binding by naming the variable _:

utop # let _ = 2 + 2 in 42;;
- : int = 42
This is seemingly not useful, but we will see later --- when we see side effects next class --- how this is very important.

Editing OCaml in VSCode

Let's create tst.ml in VSCode, and fill it with the following contents:

tst.ml
let x = 3

let y = x + 1

let _ = 
    (* Print the value of y *)
    print_endline ("Value of y: " ^ string_of_int y)
(Ignore the last three lines for now; we will revisit this next class.) Note that the ;; separator is not required in ml files. Also, see that comments are written in OCaml like (* ... *).

Now, hover over various variables to see their types. You should see, for example, that x has type int, while "Value of y: " has type string. If you hover over +, you will see it has type int -> int -> int; this is the type of a function that takes two integers and returns an integer. You will also see some helpful documentation. Similarly, if you hover over string_of_int, you will see it has type int -> string.

To run it, we have a few options:

  • Use ocamlc to compile it:
    ocamlc tst.ml -o tst
    ./tst
    
  • Interpret it using utop:
    utop tst.ml
    
  • Load it using utop, which will make the bindings available in the REPL:
    utop # #use "tst.ml";;
    val x : int = 3
    val y : int = 4
    Value of y: 4
    - : unit = ()
    
    utop # x + y;;
    - : int = 7
    

Functions

So far, we have seen the basic OCaml types int, bool, and string. Now, let's look at functions. OCaml, being a functional programming language, makes functions very easy to use and manipulate. To create a function, we can use the syntax fun (x : t) -> e, where t is a type and e is an expression:

utop # fun (x : int) -> x + 1;;
- : int -> int = <fun>
Type annotations

We will see later in the class that type annotations can be optional. For now, we will use them throughout.

The value fun (x : int) -> x + 1 is an OCaml value, just as 3 or "hi" is. However, unlike int or string, we get a function type, written t1 -> t2, where t1 and t2 are types. The <fun> syntax is OCaml's way of printing the value of the function.

Functions can be applied to arguments by writing them next to each other (and inserting parentheses where needed):

utop # (fun (x : int) -> x + 1) 3;;
- : t = 4

Note that we needed to wrap the fuction in parentheses; if we didn't, we would get a type error:

utop # fun (x : int) -> x + 1 3;;
Error: This expression has type int
       This is not a function; it cannot be applied.

This is because OCaml would parse this as fun x : int -> x + (1 3), and this would mean treating 1 as a function and applying it to the argument 3.

Because functions are values, we can manipulate them just like other values, including let-binding them:

utop # let f = fun (x : int) -> x + 1;;
val f : int -> int = <fun>

utop # f 2;;
- : int = 3

utop # let my_fun = 
        let f = fun (x : int) -> x + 1  in   
        let g = fun (y : int) -> f y + 1 in 
        fun (z : int) -> g (g z);;
val my_fun : int -> int = <fun>

utop # my_fun 2;;
- : int = 6