Basics

Fun and Profit with OCaml

OCaml Basics

Part 1 of 3

Let's begin with a tour through the basics of the OCaml programming language.

Why OCaml

Why OCaml

Ultimately, functional programming offers an alternative way to think about programming, which is useful even if you don't intend to regularly use a functional programming language. That said, ideas associated with functional programming (immutability, first-class functions, algebraic data types, pattern matching and type inference) also appear in languages such as C++, Java, Python, Rust, Kotlin and Elixir.

Variables

Let binding

At its simplest, a variable is an identifier whose meaning is bound to a particular value. In OCaml these bindings are introduced using the let keyword.

Let binding

let pi = 3

Every variable binding has a scope, which is the portion of the code that can refer to that binding. The scope of top-level let bindings (like the one above) is everything that follows it.

2 * pi * 5

Primitive data types

OCaml offers the following primitive data types: int, float, bool, char, string and unit.

Primitive data types

let one = 1 let pi = 3.1415 let a = 'a' let hello = "Hello"
let one = 1 let pi = 3.1415 let are_you_awesome = true let a = 'a' let hello = "Hello" let unit = ()

Operators and comparisons

OCaml deliberately keeps integer and floating-point arithmetic separate. This catches accidental mixing instead of silently converting one kind of number into the other. The common operators are:

Operators and comparisons

let int_arithmetic = (7 + 3, 7 - 3, 7 * 3, 7 / 3) let float_arithmetic = (7. +. 3., 7. -. 3., 7. *. 3., 7. /. 3.) let comparisons = (3 = 3, 3 <> 4, 3 < 4) let logic = true && (false || not false)

The == operator tests physical identity, a lower-level question about whether two values are represented by the same object. It is rarely what a beginner wants; use = for ordinary equality comparisons. OCaml also has no implicit conversion between int and float, so convert explicitly with float_of_int or int_of_float when necessary.

Observe that the types are inferred. One of the key features of OCaml is type inference and type checking. For example, checking the equality of incompatible types fails with a compile time error.

one = pi
Line 1, characters 6-8:
Error: This expression has type float but an expression was
       expected of type int

The bindings above include let a = 'a'. Suppose we now add let b = 'a'. What happens when OCaml evaluates a = b?

  • It evaluates to true: both are char values, and = compares the values they hold.
  • It fails to compile, the same way one = pi does.
  • It evaluates to false, because a and b are two different bindings.
  • It evaluates to true only because b was defined using the letter a.

Why: one = pi is rejected because int and float are different types, so = has nothing to compare. Here both a and b have type char, so = is well typed and simply asks whether the two values are the same: they are both the character 'a', so the answer is true. Note that = compares values, not bindings: it does not matter that a and b are separate names, or how each one was written.

Given let one = 1 and let one' = 1. (note the trailing dot on the second one') What happens when OCaml evaluates one = one'?

  • It fails to compile: 1. is a float literal, so one' is a float while one is an int.
  • It evaluates to true: both bindings are the number one.
  • It is a syntax error: ' may only be used to write a character literal such as 'a'.
  • It evaluates to false, because one and one' are different names.

Why: Two things are easy to misread here. First, ' is a perfectly legal character inside an identifier as long as it is not the first one, so one' (read aloud as "one prime") is an ordinary variable name. Second, the trailing dot in 1. makes it a float literal, exactly like 1.0; only that one character separates it from the int literal 1. So this is the same situation as one = pi: = needs both sides to have the same type, and the compiler rejects it with "This expression has type float but an expression was expected of type int."

Local let bindings

We can also use let to create a variable binding whose scope is limited to a particular expression using the in keyword:

Local let ... in binding

let i = let j = 5 in j + 2

As you can see from the output, only i has been bound to a value at the top-level. The j variable is no longer in scope:

j+4

After evaluating let i = let j = 5 in j + 2, what happens if you then try to evaluate j + 4 at the top level?

  • Compile error: j is unbound outside the let ... in expression.
  • It evaluates to 9, since j was 5.
  • It evaluates to 4, treating the missing j as 0.
  • It silently shadows i.

Why: j is local to the body of the let ... in expression. Once that expression finishes evaluating, j goes out of scope; only i (bound to 7) remains visible at the top level. Referring to j afterwards is a compile-time "Unbound value j" error.

Conditionals

OCaml provides conditional expressions using the if keyword:

Conditionals

let a = if i < 10 then i else 10
let a = if i < 10 then i else 10

Functions

Function definition

The let keyword can also be used to define functions:

Function definition

let succ x = x + 1

You can also provide explicit type annotations, but generally we elide them.

let succ (x : int) : int = x + 1

The latter definition of succ shadows the former.

Multiple arguments

Functions with multiple arguments are defined the same way:

Multiple arguments

let add x y = x + y
let add x y = x + y

Function application

Function application is written by placing each argument after the function name. Parentheses group expressions; they are not special function-call syntax:

Function application

let b = succ 8 let c = add a b
let b = succ 8 let c = add a b

Implement sum_of_succ : int -> int -> int, which computes the sum of the successors of its two arguments, using only add and succ (no +).

let sum_of_succ x y = failwith "not implemented"
Show reference solution

Apply succ to each argument first, then combine the results with add.

let sum_of_succ x y = add (succ x) (succ y)

Recursive functions

We can also create recursive functions by adding the rec keyword to a let binding. For example, the sum of first n integers can be implemented as follows:

Recursive functions

let rec sum_of_first_n n = if n <= 0 then 0 else sum_of_first_n (n-1) + n
let rec sum_of_first_n n = if n <= 0 then 0 else sum_of_first_n (n-1) + n assert (sum_of_first_n 5 = 15)

Implement a recursive function fib : int -> int that computes the nth Fibonacci number, using the convention fib 0 = 1, fib 1 = 1, and fib n = fib (n - 1) + fib (n - 2) for n >= 2.

let rec fib n = failwith "not implemented"
Show reference solution let rec fib n = if n < 2 then 1 else fib (n - 1) + fib (n - 2)

Labelled arguments

Consider the following function

let divide dividend divisor = dividend / divisor

Looking at just the signature, it's not obvious which int argument is the dividend and which is the divisor.

We can fix this using labelled arguments. To label an argument in a signature, NAME: is put before the type. When defining the function, we put a tilde (~) before the name of the argument:

Labelled arguments

let divide ~dividend ~divisor = dividend / divisor

We can then call it using:

divide ~dividend:9 ~divisor:3

Labelled arguments can be passed in any order.

divide ~divisor:3 ~dividend:9

We can also pass variables into the labelled argument:

let to_divide = 9 in let divide_by = 3 in divide ~dividend:to_divide ~divisor:divide_by

The label and the parameter used inside a function can also be written separately. Formatting is disabled for this example so the explicit spelling remains visible:

[@@@ocamlformat "disable"] let divide_explicit ~dividend:dividend ~divisor:divisor = dividend / divisor

When the label and parameter have the same name, OCaml lets us omit the repeated name. This is called label punning:

let dividend = 9 in let divisor = 3 in divide ~dividend ~divisor

Implement modulo ~dividend ~divisor, which returns the remainder of dividend divided by divisor, built using our labelled divide function (do not use OCaml's built-in mod operator).

let modulo ~dividend ~divisor = failwith "not implemented"
Show reference solution

divide already gives the truncated quotient, so the remainder is dividend - divisor * quotient.

let modulo ~dividend ~divisor = dividend - divisor * (divide ~dividend ~divisor)

Higher-order functions

Since OCaml is a functional language, functions are regular values which can be used like any other. In particular, they can be used as arguments to other functions. Functions which take other functions as arguments are called higher-order functions.

For example, the List.map function takes two arguments: a function and a list, and returns a new list created by applying the function to each of the elements of the list.

We can use List.map to apply the succ function to all the numbers in the list [1; 2; 3]:

Higher-order functions

let l = List.map succ [1;2;3]

The full set of functions available on List (and every other standard library module) is documented in the OCaml manual's standard library reference. It is worth keeping this open while you work through the workshop.

You can also check the type of a function without applying it by typing it into utop followed by ;;. For example, typing List.map;; prints its inferred type, ('a -> 'b) -> 'a list -> 'b list, directly. utop itself has its own set of features (tab completion, a command history, #show) documented at the utop repository.

List.map

Currying

Like many functional languages, OCaml provides support for partial application of functions in the form of currying.

You may have noticed that the type of our add function was written:

int -> int -> int

another way to write this type would be

int -> (int -> int).

In other words, add is actually a function which takes an int and returns a function from int to int. For example, we could redefine our succ function by partially applying add to 1:

Currying

let succ = add 1

Given let add x y = x + y has type int -> int -> int, what is the type of add 1?

  • int: applying add to one argument is a type error.
  • int -> int: a function still waiting for the second argument.
  • int -> int -> int, unchanged, since add needs both arguments at once.
  • unit, since the application is incomplete.

Why: add's type int -> int -> int is really int -> (int -> int): a function that takes an int and returns another function int -> int. Applying add to just 1 supplies the first argument and returns that inner function, which is exactly how let succ = add 1 works.

Anonymous functions

Instead of defining each function with a let, it is often handy to define functions on the fly. OCaml has support for anonymous functions, which allows you to define unnamed functions. To write an anonymous function, the fun keyword is used in the following form (fun ARG1 ARG2 ... -> BODY). We can define an anonymous function for succ and use it as follows:

Anonymous functions

List.map (fun x -> x + 1) [1;2;3]
List.map (fun x -> x + 1) [1;2;3]