Practice: pattern matching, by hand
This is a Practice chapter, not a Tutorial. There are no slides
and there is no video; it is a worksheet. The
tutorial walked through a worked example on
screen. Here you solve the problems yourself, directly in the
browser. Each problem has an editable cell seeded with
failwith "not implemented" and a test cell that prints
all tests passed when your solution is correct. A reference
solution sits below each problem behind a collapsed Reference
solution panel: try the problem first, then reveal the solution to
compare.
The rule for this worksheet: solve everything with match and
recursion by hand. The higher-order helpers (map, filter,
fold) come later; here the point is to see the recursion you are
hand-writing, one clause per shape of the data. If you find yourself
wanting List.map, stop and write the two list clauses instead.
The worksheet comes in three parts:
- Part 1: lists (Problems 1 to 7). Matching
[]againstx :: xs, carrying an index or a running answer, and one merge puzzle. - Part 2: binary trees (Problems 8 to 10). The same
'a treeyou met in the recursive-patterns lecture, matched onLeafagainstNode. - Part 3: your own variants (Problems 11 to 12). Define a small variant type and match on it, including matching on a pair of values at once.
Difficulty rises roughly as you go, but not strictly; if you get stuck on one, skip ahead and come back.
Part 1: lists
Problem 1: last
Write a function
last : 'a list -> 'a option
that returns the last element of a list as Some x, or None if
the list is empty. For example, last [1; 2; 3] = Some 3.
Implement last.
Show reference solution
Reference solution:
let rec last l =
match l with
| [] -> None
| [x] -> Some x
| _ :: xs -> last xs
Three shapes: the empty list (no last element), a one-element list
(that element is the last), and a longer list (drop the head and
recurse). The [x] clause must come before the _ :: xs clause,
since a singleton also matches _ :: xs.
Problem 2: nth
Write a function
nth : 'a list -> int -> 'a option
that returns Some of the element at position n (counting from
0), or None if n is out of range. For example,
nth [10; 20; 30] 1 = Some 20 and nth [10; 20; 30] 5 = None.
Implement nth.
Show reference solution
Reference solution:
let rec nth l n =
match l with
| [] -> None
| x :: rest -> if n = 0 then Some x else nth rest (n - 1)
Walk down the list and the index together: when n reaches 0 the
head is the answer; otherwise drop the head and look for position
n - 1 in the tail. Running off the end of the list (the []
clause) means n was too big.
Problem 3: count_occurrences
Write a function
count_occurrences : 'a -> 'a list -> int
that counts how many times a value appears in a list. For example,
count_occurrences 2 [1; 2; 2; 3; 2] = 3.
Implement count_occurrences.
Show reference solution
Reference solution:
let rec count_occurrences x l =
match l with
| [] -> 0
| y :: ys ->
(if x = y then 1 else 0) + count_occurrences x ys
The empty list contributes nothing. For a cons, add 1 when the
head equals x (and 0 otherwise), then recurse on the tail. The
comparison x = y is OCaml's structural equality, so this works for
any type whose values can be compared with =.
Problem 4: take
Write a function
take : int -> 'a list -> 'a list
that returns the first n elements of a list (all of them if the
list is shorter than n, and [] if n <= 0). For example,
take 2 [1; 2; 3; 4] = [1; 2].
Implement take.
Show reference solution
Reference solution:
let rec take n l =
match l with
| [] -> []
| _ when n <= 0 -> []
| x :: rest -> x :: take (n - 1) rest
Two ways to stop: the list runs out ([]), or we have taken enough
(n <= 0, expressed as a guard). Otherwise keep the head and take
n - 1 from the tail. The guarded clause sits before the
x :: rest clause so that n <= 0 wins even on a non-empty list.
Problem 5: drop
Write the companion to take:
drop : int -> 'a list -> 'a list
returning the list with its first n elements removed (the whole
list if n <= 0, and [] if n exceeds the length). For example,
drop 2 [1; 2; 3; 4] = [3; 4].
Implement drop.
Show reference solution
Reference solution:
let rec drop n l =
match l with
| [] -> []
| _ when n <= 0 -> l
| _ :: rest -> drop (n - 1) rest
Mirror of take: when n <= 0 we are done dropping and return the
list unchanged; otherwise discard the head and drop n - 1 more.
Note the guarded clause returns l (the whole remaining list), not
[].
Problem 6: is_sorted
Write a function
is_sorted : int list -> bool
that returns true when a list is in non-decreasing order. The
empty list and any one-element list count as sorted. For example,
is_sorted [1; 2; 2; 3] = true and is_sorted [3; 1] = false.
Implement is_sorted. You will want to look at the first two
elements at once.
Show reference solution
Reference solution:
let rec is_sorted l =
match l with
| [] | [_] -> true
| x :: (y :: _ as rest) -> x <= y && is_sorted rest
An or-pattern handles the two "trivially sorted" shapes together:
[] and a singleton [_]. For a list with at least two elements,
name the head x, the second element y, and (with as) the whole
tail rest; the list is sorted when x <= y and the tail is
sorted. Binding the tail with as lets us recurse on it without
rebuilding y :: ....
Problem 7: merge
Write a function
merge : int list -> int list -> int list
that merges two already-sorted lists into one sorted list. For
example, merge [1; 3; 5] [2; 4] = [1; 2; 3; 4; 5]. (This is the
combining step of merge sort.)
Implement merge. Assume both inputs are sorted.
Show reference solution
Reference solution:
let rec merge xs ys =
match xs, ys with
| [], _ -> ys
| _, [] -> xs
| x :: xs', y :: ys' ->
if x <= y then x :: merge xs' ys
else y :: merge xs ys'
Match on the pair of lists. If either is empty, the answer is the
other. Otherwise compare the two heads: emit the smaller one and
recurse, advancing only the list it came from. Using <= (not <)
keeps equal elements in a stable order and handles duplicates.
Part 2: binary trees
For the next three problems we use the same binary tree from the recursive-patterns lecture. Run the cell below first so the type and a sample tree are in scope.
Problem 8: sum_tree
Write a function
sum_tree : int tree -> int
that adds up every value stored in the tree (0 for an empty tree).
For sample above, sum_tree sample = 11.
Implement sum_tree.
Show reference solution
Reference solution:
let rec sum_tree t =
match t with
| Leaf -> 0
| Node (l, v, r) -> sum_tree l + v + sum_tree r
A Leaf holds nothing, so it sums to 0. A Node contributes its
own value v plus the sums of both subtrees. The two recursive
calls mirror the two subtrees: this is the tree version of summing a
list.
Problem 9: tree_max
Write a function
tree_max : int tree -> int option
that returns the largest value in the tree as Some, or None for
an empty tree. For sample, tree_max sample = Some 5.
Implement tree_max. You will need to combine option results from
the two subtrees.
Show reference solution
Reference solution:
let rec tree_max t =
match t with
| Leaf -> None
| Node (l, v, r) ->
let best a b =
match a, b with
| None, o | o, None -> o
| Some x, Some y -> Some (if x > y then x else y)
in
best (Some v) (best (tree_max l) (tree_max r))
A Leaf has no maximum (None). For a Node, the answer is the
largest of v, the left maximum, and the right maximum, each of
which is an int option. The local helper best merges two
options: if one is None the other wins (the or-pattern
None, o | o, None), and when both are Some it keeps the larger.
Problem 10: tree_contains
Write a function
tree_contains : 'a -> 'a tree -> bool
that returns true when the value appears anywhere in the tree. For
sample, tree_contains 5 sample = true and tree_contains 9 sample = false.
Implement tree_contains.
Show reference solution
Reference solution:
let rec tree_contains x t =
match t with
| Leaf -> false
| Node (l, v, r) ->
v = x || tree_contains x l || tree_contains x r
An empty tree contains nothing. A node contains x if its own value
matches, or if either subtree contains it. The || operator
short-circuits, so once the value is found the remaining subtrees are
not searched. (This is an arbitrary tree; it does not assume the
ordering of a search tree.)
Part 3: your own variants
Problem 11: beats
Define a type for the three moves of rock-paper-scissors and a function
beats : move -> move -> bool
that returns true when the first move beats the second. Rock beats
scissors, scissors beats paper, and paper beats rock; anything else
(including a tie) is false.
The type is given. Implement beats by matching on the pair of
moves.
Show reference solution
Reference solution:
let beats a b =
match a, b with
| Rock, Scissors | Paper, Rock | Scissors, Paper -> true
| _ -> false
Match on the pair a, b. An or-pattern lists the three winning
combinations on a single clause; the wildcard _ catches every
other pair (losses and ties) and returns false. Without the three
winning pairs spelled out, you would need a clause per case; the
or-pattern keeps it to two clauses.
Problem 12: nat (the natural numbers)
Here is a type that builds the natural numbers from scratch: Zero,
and Succ n (the successor, "one more than n"). So 2 is
Succ (Succ Zero). Implement two functions:
to_int : nat -> int
add : nat -> nat -> nat
to_int converts a nat to an ordinary int, and add adds two
naturals (returning a nat).
The type is given. Implement to_int and add by matching on the
constructors.
Show reference solution
Reference solution:
let rec to_int n =
match n with
| Zero -> 0
| Succ m -> 1 + to_int m
let rec add a b =
match a with
| Zero -> b
| Succ m -> Succ (add m b)
to_int peels one Succ at a time, adding 1 for each, until it
reaches Zero. add recurses on its first argument: adding Zero
to b gives b, and adding Succ m to b is one more than adding
m to b, i.e. Succ (add m b). This is addition defined purely by
pattern matching on the structure of a number.