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

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 and Primitive types

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 x = 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 * x * 5

Floats and ints

Floats and ints

let x = 3 let pi = 3.1415

Observe that the types are inferred. One of the key features of OCaml is type inference and type checking.

Working with numbers

Working with numbers

let x = 7 + 3 let y = 7. +. 3.

OCaml deliberately keeps integer and floating-point arithmetic separate. This catches accidental mixing instead of silently converting one kind of number into the other. There is no implicit conversion between int and float, so convert explicitly with float_of_int or int_of_float when necessary.

For example, checking the equality of incompatible types fails with a compile time error.

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

Compute the area of this circle that has a radius of 6.0 units (hint: area = π × r²)

let pi = 3.1415 let radius = 6.0 let area = failwith "not implemented"
Exception: Failure "not implemented".
Show reference solution let pi = 3.1415 let radius = 6.0 let area = pi *. radius *. radius

Comparisons and booleans

Comparisons and booleans

let eq = (3 = 3) let are_you_awesome = true 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.

Characters and strings

Characters and strings

let a = 'a' let hello = "Hello"

Unit

Unit

let () = print_endline "hello"

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

(* area of a square *) let area = let side = 5 in side * side

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

side + 1
let area = let side = 5 in side * side

After evaluating this, what happens if you try to evaluate side + 1 at the top level?

  • Compile error: side is unbound outside the let ... in expression.
  • It evaluates to 6, since side was 5.
  • It evaluates to 1, treating the missing side as 0.
  • It silently shadows area.

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

Conditionals

OCaml provides conditional expressions using the if keyword:

Conditionals

let time_of_day = 14 (* 14 hours = 2 pm *) let greeting = if time_of_day < 12 then "Good morning" else "Good afternoon"
let time_of_day = 14 (* 14 hours = 2 pm *) let greeting = if time_of_day < 12 then "Good morning" else "Good afternoon"

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

Implement max_of_two : int -> int -> int, which returns the larger of its two arguments.

let max_of_two x y = failwith "not implemented"
Show reference solution let max_of_two x y = if x > y then x else 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 a = succ 8 let b = add a 2
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)

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):

Anonymous functions

let succ = fun x -> x + 1 let nine = (fun x -> x + 1) 8
let succ = fun x -> x + 1 let nine = (fun x -> x + 1) 8

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] let l2 = List.map (fun x -> x * 2) [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.