OCaml Module 5 – Lists, higher-order functions and laziness
Folds over trees, options and other structures
Write fold, map and iter for OCaml trees, express size and traversals as folds, chain Option.bind instead of nested matches, and prove laws by induction.
What you will learn
- Write fold, map and iter for your own tree types
- Express size, height, sum and traversal orders as folds
- Use Option.map, Option.bind and Option.fold instead of nested matches
- Prove properties of tree functions by structural induction
Before you start
What you’ll learn · Before you start
What you will learn
- Write fold, map and iter for your own tree types
- Express size, height, sum and traversal orders as folds
- Use Option.map, Option.bind and Option.fold instead of nested matches
- Prove properties of tree functions by structural induction
Before you start
On this page
One function, fold, gave the size, the height, the sum and the sorted contents of a tree of five numbers: 5, 3, 21
and [1; 3; 4; 5; 8], each from one line that says what an empty tree is worth and how a node combines its parts.
Written once for a type, a fold saves rewriting the same recursion for every question you ask of it.
You will write fold, map and iter for a tree type, express its measures and traversal orders as folds, replace
nested matches on options with Option.bind and its friends, and prove that a law holds for every tree by structural
induction.
A fold replaces each constructor with a function
The tree type of the variants lesson has two constructors,
Leaf and Node (l, v, r). A fold over it takes one
argument per constructor: ~leaf, the value to use for an empty tree, and ~node, a function that gets the folded
left subtree, the node’s own value and the folded right subtree. fold ~leaf ~node then walks the tree once and
returns what those two arguments build. The session folds the search tree made by inserting 5, 3, 8, 1 and 4:
from session import toplevel, write
# A fold over a tree replaces Leaf with a value and every Node with a function of the folded left tree, the value and
# the folded right tree. size, height, sum and the in-order list are then one line each. The tree is the search tree
# built by inserting 5, 3, 8, 1 and 4.
write("tree.ml", """type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
let rec insert x = function
| Leaf -> Node (Leaf, x, Leaf)
| Node (l, v, r) as t ->
if x < v then Node (insert x l, v, r)
else if x > v then Node (l, v, insert x r)
else t
let rec fold ~leaf ~node = function
| Leaf -> leaf
| Node (l, v, r) -> node (fold ~leaf ~node l) v (fold ~leaf ~node r)
""")
toplevel("""
#use "tree.ml";;
let t = List.fold_left (fun t x -> insert x t) Leaf [5; 3; 8; 1; 4];;
let size t = fold ~leaf:0 ~node:(fun l _ r -> l + 1 + r) t;;
let height t = fold ~leaf:0 ~node:(fun l _ r -> 1 + max l r) t;;
let sum t = fold ~leaf:0 ~node:(fun l v r -> l + v + r) t;;
let to_list t = fold ~leaf:[] ~node:(fun l v r -> l @ [v] @ r) t;;
(size t, height t, sum t, to_list t);;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# #use "tree.ml";;
type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
val insert : 'a -> 'a tree -> 'a tree = <fun>
val fold : leaf:'a -> node:('a -> 'b -> 'a -> 'a) -> 'b tree -> 'a = <fun>
# let t = List.fold_left (fun t x -> insert x t) Leaf [5; 3; 8; 1; 4];;
val t : int tree =
Node (Node (Node (Leaf, 1, Leaf), 3, Node (Leaf, 4, Leaf)), 5,
Node (Leaf, 8, Leaf))
# let size t = fold ~leaf:0 ~node:(fun l _ r -> l + 1 + r) t;;
val size : 'a tree -> int = <fun>
# let height t = fold ~leaf:0 ~node:(fun l _ r -> 1 + max l r) t;;
val height : 'a tree -> int = <fun>
# let sum t = fold ~leaf:0 ~node:(fun l v r -> l + v + r) t;;
val sum : int tree -> int = <fun>
# let to_list t = fold ~leaf:[] ~node:(fun l v r -> l @ [v] @ r) t;;
val to_list : 'a tree -> 'a list = <fun>
# (size t, height t, sum t, to_list t);;
- : int * int * int * int list = (5, 3, 21, [1; 3; 4; 5; 8])
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tree_fold_session.py
size used ~leaf:0 and fun l _ r -> l + 1 + r, and gave 5. height replaced the addition with
1 + max l r and gave 3. sum added the values, 21, and to_list joined l @ [v] @ r into [1; 3; 4; 5; 8]. The
type of fold that the toplevel printed, leaf:'a -> node:('a -> 'b -> 'a -> 'a) -> 'b tree -> 'a, shows why one
function serves all four: the result type 'a can be anything, an int, a list or another tree.
A node can only be combined after both of its subtrees are, so a fold finishes a node’s children first. That order is the post-order traversal; step through it on the same five numbers:
- being compared or looked at
- being moved or written
- in its final place
Pseudo-code
-
postorder(n): if n = null: return -
postorder(n.left) -
postorder(n.right) -
visit n
Post-order visits a node after both subtrees: the order to delete a tree or add up sizes, because children are done first.
When it finishes: 1 4 3 8 5.
Try your own input
Key idea
A fold over a type takes one argument per constructor and replaces each constructor with it, so one recursive function serves every measure of the type.
Traversals and map are folds that place the value differently
The three depth-first orders differ in one thing: where the node’s own value goes between the two folded subtrees.
Before them gives pre-order, between them in-order, after them post-order. A fold whose ~leaf is Leaf and whose
~node rebuilds a Node is map for trees:
from session import toplevel, write
# The three depth-first orders differ only in where the node's own value goes, and map for a tree is a fold as well.
# iter is written with ; so that the order of its effects is fixed: a fold computes both subtrees before it calls
# node, in an order OCaml does not specify.
write("tree.ml", """type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
let rec insert x = function
| Leaf -> Node (Leaf, x, Leaf)
| Node (l, v, r) as t ->
if x < v then Node (insert x l, v, r)
else if x > v then Node (l, v, insert x r)
else t
let rec fold ~leaf ~node = function
| Leaf -> leaf
| Node (l, v, r) -> node (fold ~leaf ~node l) v (fold ~leaf ~node r)
""")
toplevel("""
#use "tree.ml";;
let t = List.fold_left (fun t x -> insert x t) Leaf [5; 3; 8; 1; 4];;
let preorder t = fold ~leaf:[] ~node:(fun l v r -> [v] @ l @ r) t;;
let inorder t = fold ~leaf:[] ~node:(fun l v r -> l @ [v] @ r) t;;
let postorder t = fold ~leaf:[] ~node:(fun l v r -> l @ r @ [v]) t;;
(preorder t, inorder t, postorder t);;
let map_tree f t = fold ~leaf:Leaf ~node:(fun l v r -> Node (l, f v, r)) t;;
map_tree (fun x -> x * 10) t;;
let rec iter f = function Leaf -> () | Node (l, v, r) -> iter f l; f v; iter f r;;
iter (fun x -> Printf.printf "%d " x) t;;
fold ~leaf:() ~node:(fun () v () -> Printf.printf "%d " v) t;;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# #use "tree.ml";;
type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
val insert : 'a -> 'a tree -> 'a tree = <fun>
val fold : leaf:'a -> node:('a -> 'b -> 'a -> 'a) -> 'b tree -> 'a = <fun>
# let t = List.fold_left (fun t x -> insert x t) Leaf [5; 3; 8; 1; 4];;
val t : int tree =
Node (Node (Node (Leaf, 1, Leaf), 3, Node (Leaf, 4, Leaf)), 5,
Node (Leaf, 8, Leaf))
# let preorder t = fold ~leaf:[] ~node:(fun l v r -> [v] @ l @ r) t;;
val preorder : 'a tree -> 'a list = <fun>
# let inorder t = fold ~leaf:[] ~node:(fun l v r -> l @ [v] @ r) t;;
val inorder : 'a tree -> 'a list = <fun>
# let postorder t = fold ~leaf:[] ~node:(fun l v r -> l @ r @ [v]) t;;
val postorder : 'a tree -> 'a list = <fun>
# (preorder t, inorder t, postorder t);;
- : int list * int list * int list =
([5; 3; 1; 4; 8], [1; 3; 4; 5; 8], [1; 4; 3; 8; 5])
# let map_tree f t = fold ~leaf:Leaf ~node:(fun l v r -> Node (l, f v, r)) t;;
val map_tree : ('a -> 'b) -> 'a tree -> 'b tree = <fun>
# map_tree (fun x -> x * 10) t;;
- : int tree =
Node (Node (Node (Leaf, 10, Leaf), 30, Node (Leaf, 40, Leaf)), 50,
Node (Leaf, 80, Leaf))
# let rec iter f = function Leaf -> () | Node (l, v, r) -> iter f l; f v; iter f r;;
val iter : ('a -> 'b) -> 'a tree -> unit = <fun>
# iter (fun x -> Printf.printf "%d " x) t;;
1 3 4 5 8 - : unit = ()
# fold ~leaf:() ~node:(fun () v () -> Printf.printf "%d " v) t;;
8 4 1 3 5 - : unit = ()
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 orders_session.py
Each order is one node function, and the session printed all three lists for its tree:
| Order | The node function | On the session’s tree |
|---|---|---|
| pre-order | [v] @ l @ r |
[5; 3; 1; 4; 8] |
| in-order | l @ [v] @ r |
[1; 3; 4; 5; 8], sorted for a search tree |
| post-order | l @ r @ [v] |
[1; 4; 3; 8; 5] |
map_tree (fun x -> x * 10) kept the shape and gave a tree of 10, 30, 40, 50 and 80. For iter, which exists only for
its effects, the session wrote the recursion by hand with ;, which fixes the order: it printed 1 3 4 5 8. The fold
version printed 8 4 1 3 5. A fold computes both subtrees as arguments of node, and OCaml leaves the order in which
arguments are evaluated unspecified; this compiler evaluated the right one first.
An iter built from a fold
Wrong: fold ~leaf:() ~node:(fun () v () -> print v) for printing in order, which printed 8 4 1 3 5. Right: write
iter with ; between the steps, iter f l; f v; iter f r, whenever the order of effects matters.
A fold over a tree is not tail-recursive: it keeps a frame for each level it is inside, so its stack use is the tree’s depth. The program below folds two trees of 1,000,000 values under a stack limited to 100,000 words:
from session import run, write
# A fold over a tree is not tail-recursive: it keeps a frame for every level it is inside. A balanced tree of 1,000,000
# values is 20 levels deep; a chain of 1,000,000 nodes, the tree sorted input gives, is 1,000,000 levels deep.
write("depth.ml", """type 'a tree = Leaf | Node of 'a tree * 'a * 'a tree
let rec fold ~leaf ~node = function
| Leaf -> leaf
| Node (l, v, r) -> node (fold ~leaf ~node l) v (fold ~leaf ~node r)
let size t = fold ~leaf:0 ~node:(fun l _ r -> l + 1 + r) t
(* A balanced tree of the values lo .. hi: the middle value at the root. *)
let rec balanced lo hi =
if lo > hi then Leaf
else
let mid = (lo + hi) / 2 in
Node (balanced lo (mid - 1), mid, balanced (mid + 1) hi)
(* A chain: every node has only a right child, built with a loop. *)
let chain n =
let t = ref Leaf in
for i = n downto 1 do t := Node (Leaf, i, !t) done;
!t
let rec depth = function Leaf -> 0 | Node (l, _, r) -> 1 + max (depth l) (depth r)
let () =
let n = 1_000_000 in
match Sys.argv.(1) with
| "balanced" ->
let t = balanced 1 n in
Printf.printf "balanced: size %d, depth %d\\n" (size t) (depth t)
| _ -> Printf.printf "chain: size %d\\n" (size (chain n))
""")
run("""
ocamlopt depth.ml -o depth
export OCAMLRUNPARAM=l=100000
./depth balanced
!./depth chain
""") Output
$ ocamlopt depth.ml -o depth $ export OCAMLRUNPARAM=l=100000 $ ./depth balanced balanced: size 1000000, depth 20 $ ./depth chain Fatal error: exception Stack_overflow [exit status 2]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 depth_session.py
The balanced tree was 20 levels deep, and its size came out as 1000000. The chain, a tree in which every node has
only a right child, which is what sorted input builds, raised Stack_overflow: a million levels need a million
frames.
Checkpoint
Check: A fold over a tree · Traversals, map, iter and depth
Option.bind chains steps that may each fail
An option is a variant with two constructors, None and Some v, and the Option module provides its maps and its
fold. The session reads a port from a list of settings, where each step can fail: the key may be missing, the text may
not be a number, and the number may be out of range. Step through the four settings lists it tries:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A good setting. For the settings list, List.assoc_opt "port" found Some "8080", int_of_string_opt turned it into Some 8080, and in_range kept it: port gave Some 8080.
- No port at all. For the empty list the lookup gave None. Option.bind does not call the next function on None, so parsing and the range check never ran: port gave None.
- Not a number. For [("port", "eighty")] the lookup found Some "eighty", and int_of_string_opt gave None. The range check was skipped and port gave None.
- Out of range. For [("port", "70000")] both earlier steps succeeded, with Some 70000, but in_range refused a number above 65535: port gave None.
- One chain, three failures. The nested match, the two Option.bind calls and the let* version all gave [Some 8080; None; None; None]. Each step only says how to go on from a value; bind handles every None.
from session import toplevel
# Reading a port from settings has three ways to fail: the key is missing, the text is not a number, or the number is
# out of range. Option.bind chains the steps, the binding operator let* writes the chain as a sequence, and
# Option.value, Option.fold and Option.map use the result.
toplevel("""
let settings = [("host", "localhost"); ("port", "8080"); ("retries", "three")];;
let in_range n = if n > 0 && n < 65536 then Some n else None;;
let port_nested s = match List.assoc_opt "port" s with None -> None | Some text -> (match int_of_string_opt text with None -> None | Some n -> in_range n);;
let port s = Option.bind (Option.bind (List.assoc_opt "port" s) int_of_string_opt) in_range;;
let ( let* ) = Option.bind;;
let port_seq s = let* text = List.assoc_opt "port" s in let* n = int_of_string_opt text in in_range n;;
let cases = [settings; []; [("port", "eighty")]; [("port", "70000")]];;
(List.map port_nested cases, List.map port cases, List.map port_seq cases);;
Option.value ~default:80 (port []);;
List.map (fun s -> Option.fold ~none:"no port" ~some:(Printf.sprintf "port %d") (port s)) cases;;
Option.map (fun p -> p + 1) (port settings);;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let settings = [("host", "localhost"); ("port", "8080"); ("retries", "three")];;
val settings : (string * string) list =
[("host", "localhost"); ("port", "8080"); ("retries", "three")]
# let in_range n = if n > 0 && n < 65536 then Some n else None;;
val in_range : int -> int option = <fun>
# let port_nested s = match List.assoc_opt "port" s with None -> None | Some text -> (match int_of_string_opt text with None -> None | Some n -> in_range n);;
val port_nested : (string * string) list -> int option = <fun>
# let port s = Option.bind (Option.bind (List.assoc_opt "port" s) int_of_string_opt) in_range;;
val port : (string * string) list -> int option = <fun>
# let ( let* ) = Option.bind;;
val ( let* ) : 'a option -> ('a -> 'b option) -> 'b option = <fun>
# let port_seq s = let* text = List.assoc_opt "port" s in let* n = int_of_string_opt text in in_range n;;
val port_seq : (string * string) list -> int option = <fun>
# let cases = [settings; []; [("port", "eighty")]; [("port", "70000")]];;
val cases : (string * string) list list =
[[("host", "localhost"); ("port", "8080"); ("retries", "three")]; [];
[("port", "eighty")]; [("port", "70000")]]
# (List.map port_nested cases, List.map port cases, List.map port_seq cases);;
- : int option list * int option list * int option list =
([Some 8080; None; None; None], [Some 8080; None; None; None],
[Some 8080; None; None; None])
# Option.value ~default:80 (port []);;
- : int = 80
# List.map (fun s -> Option.fold ~none:"no port" ~some:(Printf.sprintf "port %d") (port s)) cases;;
- : string list = ["port 8080"; "no port"; "no port"; "no port"]
# Option.map (fun p -> p + 1) (port settings);;
- : int option = Some 8081
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 option_session.py
All three versions gave [Some 8080; None; None; None]. The nested match needs a None -> None for every step.
Option.bind o f is None when o is None and f v when o is Some v, so two calls of it chain the three
steps. The binding operator: An operator such as let* that the programmer defines and OCaml uses to read let* x = e in body as a call of that operator on e and fun x -> body; defined as Option.bind, it chains steps that may give None. let*, defined as Option.bind, writes the same chain
as a sequence of lets that stops at the first None.
The session used four functions of the Option module, bind among them, each for its own job:
| Function | Use it to | In the session |
|---|---|---|
Option.map f o |
apply f inside, keeping None |
Some 8080 became Some 8081 |
Option.bind o f |
chain a step that may fail | three steps, one result |
Option.value ~default o |
leave the option with a fallback | port [] gave 80 |
Option.fold ~none ~some o |
handle both cases in one call | "port 8080" and "no port" |
map or bind
Use Option.map when the step always succeeds and Option.bind when the step itself returns an option. Mapping a
step that returns an option gives an int option option, a sign that bind was meant.
Structural induction proves a law for every tree
A test checks the trees it tries; a proof covers all of them. structural induction: A way to prove a claim about every value of a recursive type: prove it for each constructor, assuming it already holds for the smaller values of the same type inside that constructor. proves a claim about every value of a type by proving it for each constructor, assuming it already
holds for the smaller values inside. Step through the proof that mirror, which swaps every left and right subtree,
keeps the size of a tree:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The claim. The claim is about every tree, and a tree is either Leaf or Node (l, v, r). So the proof has one case for each constructor, the same two cases that size and mirror have.
- The base case. mirror Leaf is Leaf by the definition of mirror, so size (mirror Leaf) = size Leaf = 0. The claim holds for the empty tree.
- The hypothesis. For a Node, assume the claim already holds for its two subtrees: size (mirror l) = size l and size (mirror r) = size r. This is the induction hypothesis.
- The step. mirror (Node (l, v, r)) is Node (mirror r, v, mirror l), whose size is size (mirror r) + 1 + size (mirror l). By the hypothesis that is size r + 1 + size l, which equals size l + 1 + size r, the size of Node (l, v, r).
- Every tree. Every tree is built from Leaf by Node, so a claim true for Leaf and kept by every Node is true for all trees. The proof has the same shape as the recursive functions it is about.
The proof has two cases because the type has two constructors, and it uses the claim for l and r exactly where
size and mirror call themselves. The session complements it by testing: it builds all 65 tree shapes with at most
five nodes and checks claims about mirror on each one:
from session import toplevel
# A proof by structural induction covers every tree; a test can cover every tree up to a size. The session builds all
# 65 tree shapes with at most 5 nodes and checks three claims about mirror on each: two hold, and one is false.
toplevel("""
type tree = Leaf | Node of tree * int * tree;;
let rec size = function Leaf -> 0 | Node (l, _, r) -> size l + 1 + size r;;
let rec mirror = function Leaf -> Leaf | Node (l, v, r) -> Node (mirror r, v, mirror l);;
let rec to_list = function Leaf -> [] | Node (l, v, r) -> to_list l @ [v] @ to_list r;;
let rec shapes n = if n = 0 then [Leaf] else List.concat_map (fun k -> List.concat_map (fun l -> List.map (fun r -> Node (l, n, r)) (shapes (n - 1 - k))) (shapes k)) (List.init n Fun.id);;
let all () = List.concat_map shapes [0; 1; 2; 3; 4; 5];;
List.length (all ());;
List.for_all (fun t -> size (mirror t) = size t) (all ());;
List.for_all (fun t -> to_list (mirror t) = List.rev (to_list t)) (all ());;
List.find_opt (fun t -> to_list (mirror t) <> to_list t) (all ());;
""") Output
$ ocaml OCaml version 5.5.1 Enter #help;; for help. # type tree = Leaf | Node of tree * int * tree;; type tree = Leaf | Node of tree * int * tree # let rec size = function Leaf -> 0 | Node (l, _, r) -> size l + 1 + size r;; val size : tree -> int = <fun> # let rec mirror = function Leaf -> Leaf | Node (l, v, r) -> Node (mirror r, v, mirror l);; val mirror : tree -> tree = <fun> # let rec to_list = function Leaf -> [] | Node (l, v, r) -> to_list l @ [v] @ to_list r;; val to_list : tree -> int list = <fun> # let rec shapes n = if n = 0 then [Leaf] else List.concat_map (fun k -> List.concat_map (fun l -> List.map (fun r -> Node (l, n, r)) (shapes (n - 1 - k))) (shapes k)) (List.init n Fun.id);; val shapes : int -> tree list = <fun> # let all () = List.concat_map shapes [0; 1; 2; 3; 4; 5];; val all : unit -> tree list = <fun> # List.length (all ());; - : int = 65 # List.for_all (fun t -> size (mirror t) = size t) (all ());; - : bool = true # List.for_all (fun t -> to_list (mirror t) = List.rev (to_list t)) (all ());; - : bool = true # List.find_opt (fun t -> to_list (mirror t) <> to_list t) (all ());; - : tree option = Some (Node (Leaf, 2, Node (Leaf, 1, Leaf)))
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 induction_session.py
size (mirror t) = size t held for all 65, as the proof says it must for every tree. So did
to_list (mirror t) = List.rev (to_list t). The third claim, that mirroring keeps the in-order list, was false, and
List.find_opt returned the first tree that breaks it, Node (Leaf, 2, Node (Leaf, 1, Leaf)), a tree of only two
nodes whose list [2; 1] becomes [1; 2]. A search over small cases finds a false law at once; only a proof shows a true one holds beyond them.
Key idea
To prove a claim for every tree, prove it for Leaf, then for Node (l, v, r) assuming it for l and r: the
proof follows the same two cases as the function.
Checkpoint
Check: Option.map, bind, value and fold · Structural induction
Practise: folds, traversals and options
The questions use the session’s tree, its folds and the port settings with new values and new laws. Work out each result, or whether a claim holds, before you check:
Practice that adapts to you
Practise this lesson
12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (2–5 years): what is a fold over a tree, and how does it relate to the functions on that tree?
A fold over a tree type takes one argument per constructor: a value for an empty tree, and a function that combines a node’s value with the already folded results of its subtrees. It walks the tree once, replacing every constructor with the matching argument. Most recursive functions on the tree are then special cases: size is a fold that adds 1 at each node, height one that takes the larger subtree plus 1, and the in-order list one that joins left, value and right. Like those functions, a tree fold is not tail-recursive, so its stack use is the depth of the tree, small for a balanced tree and as large as the tree for a chain.
Two harder questions follow, about an expression tree too deep for its fold and about a chain of optional steps that must say which step failed. For each, say what you would measure or print first, then open its first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A calculator parses formulas into an expression tree and evaluates them with a fold over the tree. CI runs the tests with a small stack limit, and a new test, a generated sum of 200,000 terms written a + b + c + …, stops with Stack_overflow. Explain why this tree is so deep, and give two fixes with their costs.
-
Fresher to mid
A support tool finds the email of a ticket assignee's manager in four steps that each return an option. Written as nested matches it is 30 lines long, and support now wants to know which step failed. Rewrite it so that a fifth step is one more line, and keep the reason for a failure.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does a fold over a type take as arguments?
One argument per constructor; it replaces each constructor with its argument, so one recursive function serves every measure of the type.
-
What did one fold compute for the lesson's tree of five numbers?
Size 5, height 3, sum 21 and the sorted contents [1; 3; 4; 5; 8], each from a one-line use of the same fold.
-
How do pre-, in- and post-order traversals differ when written as folds?
Only in where the node's value goes between the folded subtrees; a tree's map is a fold that rebuilds each node.
-
Why is an in-order iter written with ; rather than built from the fold?
A fold leaves the order of evaluating the two subtrees to the compiler: the fold-based print gave 8 4 1 3 5. iter f l; f v; iter f r fixes the order of the effects.
-
How much stack does a fold over a tree use?
In proportion to the tree's depth: little for a balanced tree, as much as the tree's size for a chain.
-
When do you use Option.map, and when Option.bind?
Option.map when the step always succeeds, Option.bind when the step itself returns an option. Mapping such a step gives an int option option, a sign that bind was meant.
-
What replaces nested matches on options in a chain of steps that may fail?
Option.bind, Option.map, Option.value and Option.fold, or let* defined as Option.bind; the chain stops at the first None.
-
How does structural induction prove a claim for every tree?
Prove it for Leaf, then for Node (l, v, r) assuming it holds for l and r: the same two cases as the function. A test over all small trees finds a false claim's counterexample.
Key takeaways
- A fold over a type takes one argument per constructor, so
size,height,sumandto_listof a tree are each one line with the samefold. - Pre-, in- and post-order differ only in where the node’s value goes between the folded subtrees, and
mapfor a tree is a fold that rebuilds each node. - A fold leaves the order of evaluating the two subtrees to the compiler, so an
iterwhose effects must come in order is written with;, and a tree fold needs stack in proportion to the tree’s depth. Option.map,Option.bind,Option.valueandOption.fold, orlet*defined asOption.bind, replace nested matches on options and stop at the firstNone.- Structural induction proves a claim for every tree by proving it for
Leafand forNodeassuming it for the subtrees, while a test over all small trees finds a false claim’s counterexample.
Terms in this lesson
- binding operator
- An operator such as let* that the programmer defines and OCaml uses to read let* x = e in body as a call of that operator on e and fun x -> body; defined as Option.bind, it chains steps that may give None.
- structural induction
- A way to prove a claim about every value of a recursive type: prove it for each constructor, assuming it already holds for the smaller values of the same type inside that constructor.
References
- The OCaml Manual: module Option (Inria and the OCaml developers)
- The OCaml Manual: Binding operators (Inria and the OCaml developers)
- Higher-Order Functions (OCaml.org)
- The OCaml Manual: The core language (Inria and the OCaml developers)
- The OCaml Manual: Expressions (function application and evaluation order) (Inria and the OCaml developers)
- The OCaml Manual: The runtime system (OCAMLRUNPARAM and the stack limit) (Inria and the OCaml developers)
- Proving properties of programs by structural induction (R. M. Burstall, The Computer Journal 12(1), 41-48) (Oxford University Press)
Report a problem with this lesson
Kept only in this browser. Your Learn progress