Data types and pattern matching

Fun and Profit with OCaml

Data Types and Pattern Matching

Part 2 of 3

OCaml has a concise and expressive system for creating new data types. Pattern matching provides a natural way to inspect and deconstruct values of those types.

Variants

Variant types

Variants represent data that can be in one of a fixed number of forms. We will use a small colour type to see why this works so well with pattern matching:

Variant types

type colour = | Red | Green | Blue let red = Red
type colour = | Red | Green | Blue let red = Red

Records

Record types

Records in OCaml represent a collection of named elements. A simple example is a point record containing x, y and z fields:

Record types

type point = { x : int; y : int; z : int; }
type point = { x : int; y : int; z : int; }

We can create instances of our point type using { ... }, and access the elements of a point using the '.' operator:

Creating and accessing records

let origin = { x = 0; y = 0; z = 0 } let get_y r = r.y let o = get_y origin

Functional update

New records can also be created from existing records using the with keyword. For example, we can create a new point which is the same as origin except with the value of its z field changed to 10:

Functional update

let p = { origin with z = 10 }

Field punning

Another useful trick with records is field punning, which allows you to replace:

Field punning

let mk_point x y z = { x; y; z }
let mk_point x y z = { x = x; y = y; z = z }

with

let mk_point x y z = { x; y; z }

Tuples

A tuple groups a fixed number of values without naming the fields. Its type uses *, and its value and pattern use commas. Tuples are useful for small, local combinations such as a pair of counters; use a record when field names would make the meaning clearer.

Tuples

let score = (3, 2) let describe_score (x, o) = Printf.sprintf "X: %d, O: %d" x o let x_score = fst score

Variants with data

Constructor arguments

Variant constructors can also have arguments. This allows variants to contain different types of data depending on which constructor was used. For example, we can create a type which contains either a point or a colour:

Constructor arguments

type t = | Point of point | Colour of colour let p_or_c cond pnt col = if cond then Point pnt else Colour col let p = p_or_c (1 > 0) origin red

Multiple constructor arguments

Variant constructors can contain multiple arguments separated by the * symbol:

Multiple constructor arguments

type s = | ThreePoints of point * point * point | TwoColours of colour * colour let s = TwoColours(Red, Green)
type s = | ThreePoints of point * point * point | TwoColours of colour * colour

Creating these constructors with multiple arguments requires parentheses:

let s = TwoColours(Red, Green)

Pattern matching

Before we go on, let us define a small helper that prints a string to the terminal.

let show s = print_endline s

Now we can print a message:

show "Hello, world!"

Inspecting variants

So far we have created some values of variant types, but how do we get the data back out of them? The answer is pattern matching. Using a match expression we can deconstruct a variant value and retrieve its constructor's arguments:

Inspecting variants

let print_t t = match t with | Point p -> show (Printf.sprintf "Point: %d %d %d" p.x p.y p.z) | Colour c -> show (Printf.sprintf "Colour")
let print_t t = match t with | Point p -> show (Printf.sprintf "Point: %d %d %d" p.x p.y p.z) | Colour c -> show (Printf.sprintf "Colour") let () = print_t (Point { x = 5; y = 9; z = 0 }) let () = print_t (Colour Blue)

Here Point p and Colour c are not expressions but patterns. They describe the shape of the data and bind variables to different parts of it. Note that the p in Point p does not refer to an existing p variable, instead it is creating a new p variable bound to the argument of the Point constructor.

Nested patterns

We can nest patterns within other patterns to do pattern matching on the constructor arguments. For example, we can print the names of the different colours in our print_t function:

Nested patterns

let print_t t = match t with | Point p -> show (Printf.sprintf "Point: %d %d %d" p.x p.y p.z) | Colour Red -> show (Printf.sprintf "Red") | Colour Green -> show (Printf.sprintf "Green") | Colour Blue -> show (Printf.sprintf "Blue")
let print_t t = match t with | Point p -> show (Printf.sprintf "Point: %d %d %d" p.x p.y p.z) | Colour Red -> show (Printf.sprintf "Red") | Colour Green -> show (Printf.sprintf "Green") | Colour Blue -> show (Printf.sprintf "Blue") let () = print_t (Colour Red) let () = print_t (Colour Blue)

Pattern guards

A branch may add a boolean condition with when. The branch is selected only when both its pattern and its guard match. Guards are useful when the shape of the data is not enough by itself:

Pattern guards

let sign n = match n with | n when n < 0 -> "negative" | 0 -> "zero" | _ -> "positive"

Matching records

We can also match on record data using the same syntax as to create records, including field punning. So our print_t can be further refined to:

Matching records

let print_t t = match t with | Point { x; y; z } -> show (Printf.sprintf "Point: %d %d %d" x y z) | Colour Red -> show (Printf.sprintf "Red") | Colour Green -> show (Printf.sprintf "Green") | Colour Blue -> show (Printf.sprintf "Blue")
let print_t t = match t with | Point { x; y; z } -> show (Printf.sprintf "Point: %d %d %d" x y z) | Colour Red -> show (Printf.sprintf "Red") | Colour Green -> show (Printf.sprintf "Green") | Colour Blue -> show (Printf.sprintf "Blue")

Exhaustiveness

A key feature of pattern matching, which can help prevent many errors especially when refactoring, is that the compiler will warn you if you forget to handle a particular case. For example, if we had forgotten the Colour Green case in the above definition:

Exhaustiveness

let print_t_ t = match t with | Point { x; y; z } -> show (Printf.sprintf "Point: %d %d %d" x y z) | Colour Red -> show (Printf.sprintf "Red") | Colour Blue -> show (Printf.sprintf "Blue")
Lines 2-5, characters 5-50:
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
  Here is an example of a case that is not matched: Colour Green
let print_t_ t = match t with | Point { x; y; z } -> show (Printf.sprintf "Point: %d %d %d" x y z) | Colour Red -> show (Printf.sprintf "Red") | Colour Blue -> show (Printf.sprintf "Blue")
Lines 2-5, characters 5-50:
Warning 8 [partial-match]: this pattern-matching is not exhaustive.
  Here is an example of a case that is not matched: Colour Green

The _ pattern

Sometimes, you do not care about a value. In that case you can use the _ pattern, which matches any value without binding it to a name:

The _ pattern

let is_colour_red t = match t with | Colour Red -> true | _ -> false

Match ordering

Note that patterns are matched from top to bottom: if a value matches multiple patterns then the first of those patterns will be selected. For example, in the following code the second case will never be matched:

Match ordering

let is_colour_red t = match t with | _ -> false | Colour Red -> true
Line 4, characters 7-17:
Warning 11 [redundant-case]: this match case is unused.
let is_colour_red t = match t with | _ -> false | Colour Red -> true
Line 4, characters 7-17:
Warning 11 [redundant-case]: this match case is unused.

Suppose print_t_ (defined earlier) omits the Colour Green case of the t variant. What does OCaml do?

  • Nothing: OCaml has no way to know a case is missing.
  • It raises a runtime exception the first time Colour Green is matched.
  • The compiler emits a "this pattern-matching is not exhaustive" warning at compile time, naming Colour Green as an example of an unmatched value.
  • It silently falls through to the Point case.

Why: the compiler knows every constructor of t and colour from their type definitions, so it can check a match against the full set of shapes a value could take. A missing case is flagged statically, before the program ever runs, which is one of pattern matching's main safety benefits over an if/else chain on manually-extracted fields.

Parameterised types

Types in OCaml can be parameterised by other types. For example, the option type which may or may not contain a value:

Parameterised types

type 'a option = | None | Some of 'a let io = Some 6 let co = Some Green
type 'a option = | None | Some of 'a

In the above the 'a is a type variable, which can be substituted by any type. For instance, we can create a value of type int option or a value of type colour option:

let io = Some 6 let co = Some Green

We can define a printer for t option type as follows:

let print_t_opt t = match t with | None -> show "None" | Some t -> print_t t let () = print_t_opt (Some (Colour Red)) let () = print_t_opt None

Polymorphic values

These type variables also appear when creating polymorphic values. For example, the following function has type 'a option -> 'a list which means it can be applied to any option type:

Polymorphic values

let opt_to_list o = match o with | Some x -> [x] | None -> []
let opt_to_list o = match o with | Some x -> [x] | None -> [] let l = opt_to_list (Some 9) let m = opt_to_list (Some Red)

Polymorphic constructors

Constructors of parameterised types which do not include the type parameter, such as None in the optional type, are also examples of polymorphic values:

Polymorphic constructors

let n = None let a = [ Some 3; n ] let b = [ n; Some Blue ]
let n = None let a = [ Some 3; n ] let b = [ n; Some Blue ]

Recursive data types

Data types in OCaml can also be recursive. This allows us to create recursive structures such as trees and lists. The following defines a parametric binary tree type:

Recursive data types

type 'a binary_tree = | Leaf | Tree of 'a binary_tree * 'a * 'a binary_tree

As you can see the Tree constructor of binary_tree contains other binary_trees as its arguments.

Inspecting recursive data types

We can write recursive functions to handle these recursive data types. For example, the following function returns the maximum depth of a binary tree:

Inspecting recursive data types

let rec depth tr = match tr with | Leaf -> 1 | Tree(left, _, right) -> 1 + (max (depth left) (depth right))
let rec depth tr = match tr with | Leaf -> 1 | Tree(left, _, right) -> 1 + (max (depth left) (depth right)) let tree : colour binary_tree = Tree(Tree(Leaf, Blue, Tree(Leaf, Red, Leaf)), Red, Tree(Leaf, Green, Leaf)) let d = depth tree

Lists

Constructing lists

A particularly common built-in data type in OCaml is the list type. list is actually a parameterised recursive variant type. It has two constructors :: (called cons) and [] (called nil). [] represents an empty list and :: adds an element to the front of the list:

Constructing lists

let l = 1 :: 2 :: 3 :: []

OCaml also provides a shorthand syntax for lists: [ ..; .. ]. Our l value above could instead have been defined:

let l = [1; 2; 3]

Matching lists

Like all constructors, the list constructors can be used as patterns in pattern matching. The following function sums all the elements of an int list:

Matching lists

let rec sum il = match il with | [] -> 0 | i :: rest -> i + (sum rest)
let rec sum il = match il with | [] -> 0 | i :: rest -> i + (sum rest) let s = sum l

Write min_list : int list -> int option to compute the minimum element of an integer list. Return None for the empty list, and Some e when the minimum element is e.

let rec min_list_helper cur_min l = match l with | [] -> cur_min | x :: xs -> (match cur_min with | None -> failwith "not implemented" | Some m -> failwith "not implemented") let min_list l = min_list_helper None l
Show reference solution

The helper's None branch should start the running minimum at x; the Some m branch should keep whichever of m and x is smaller.

let rec min_list_helper cur_min l = match l with | [] -> cur_min | x :: xs -> (match cur_min with | None -> min_list_helper (Some x) xs | Some m -> min_list_helper (Some (min m x)) xs) let min_list l = min_list_helper None l

Write postfix : 'a binary_tree -> 'a list that returns the elements of a binary tree in postfix order (left subtree, then right subtree, then the node itself). Use the list append operator @.

let rec postfix t = failwith "not implemented"
Show reference solution let rec postfix t = match t with | Leaf -> [] | Tree (left, v, right) -> postfix left @ postfix right @ [v]

Write rev_list : 'a list -> 'a list that reverses a list. Use the list append operator @ (an O(n) accumulator-based version is a nice follow-up once you've seen this one work).

let rec rev_list l = failwith "not implemented"
Show reference solution let rec rev_list l = match l with | [] -> [] | x :: xs -> rev_list xs @ [x]

Optional: mutation and references

Most of the workshop uses immutable values. OCaml also supports mutation when changing state in place is the clearer tool.

Optional: mutation

type mpoint = { mutable x : int; mutable y : int; mutable z : int } let p = { x = 0; y = 0; z = 10 } let () = p.z <- 20

Reference cells provide one mutable value without defining a new record type:

Optional: references

let counter = ref 0 let () = counter := !counter + 1 let seen = !counter

Optional: type aliases

An alias gives an existing type another name. It does not create a distinct type:

Optional: type aliases

type int_pair = int * int let id (x : int_pair) = x