OCaml Module 5 – Lists, higher-order functions and laziness
Map, filter and fold
Replace hand-written OCaml list recursion with map, filter, filter_map and fold, see how fold_left and fold_right differ, and build pipelines with |>.
What you will learn
- Use List.map, filter, filter_map, concat_map and partition instead of explicit recursion
- Explain how fold_left and fold_right differ in order and in stack use
- Derive map and filter from a fold, and apply laws such as map fusion
- Build small data pipelines over lists with the |> operator
Before you start
What you’ll learn · Before you start
What you will learn
- Use List.map, filter, filter_map, concat_map and partition instead of explicit recursion
- Explain how fold_left and fold_right differ in order and in stack use
- Derive map and filter from a fold, and apply laws such as map fusion
- Build small data pipelines over lists with the |> operator
Before you start
On this page
List.fold_left ( - ) 10 [1; 2; 3] gives 4, and List.fold_right ( - ) [1; 2; 3] 10 gives -8: the same list, the
same function, the same start value, two answers. Most loops over a list are a map, a filter or a fold, and knowing
which fold you hold decides both the answer and whether a list of a million elements fits on the stack.
You will replace hand-written recursion with the List module’s higher-order functions, explain how the two folds
differ, write map and filter as folds and merge two passes into one, and build pipelines over lists with |>.
Map, filter and their relatives replace the usual recursions
Most recursive functions over a list do one of a few jobs: change every element, keep some of them, or both. The List module has a higher-order function: A function that takes another function as an argument or returns one, such as List.map, which applies the function it is given to every element of a list. for each, a function that takes another function, the step, as its argument and does the walking itself. The session runs each one on four students’ marks:
from session import toplevel
# Higher-order functions of the List module replace the usual recursions: map transforms every element, filter keeps
# some, filter_map does both with a function that may fail, partition splits a list in two and concat_map joins the
# lists a function returns.
toplevel("""
let marks = [("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)];;
List.map snd marks;;
List.filter (fun (_, m) -> m >= 40) marks;;
List.partition (fun (_, m) -> m >= 60) marks;;
let raw = ["72"; "absent"; "91"; ""; "58"];;
List.filter_map int_of_string_opt raw;;
List.map int_of_string_opt raw;;
List.concat_map (fun (name, m) -> if m >= 90 then [name; "distinction"] else [name]) marks;;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let marks = [("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)];;
val marks : (string * int) list =
[("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)]
# List.map snd marks;;
- : int list = [72; 35; 91; 58]
# List.filter (fun (_, m) -> m >= 40) marks;;
- : (string * int) list = [("asha", 72); ("meera", 91); ("kiran", 58)]
# List.partition (fun (_, m) -> m >= 60) marks;;
- : (string * int) list * (string * int) list =
([("asha", 72); ("meera", 91)], [("ravi", 35); ("kiran", 58)])
# let raw = ["72"; "absent"; "91"; ""; "58"];;
val raw : string list = ["72"; "absent"; "91"; ""; "58"]
# List.filter_map int_of_string_opt raw;;
- : int list = [72; 91; 58]
# List.map int_of_string_opt raw;;
- : int option list = [Some 72; None; Some 91; None; Some 58]
# List.concat_map (fun (name, m) -> if m >= 90 then [name; "distinction"] else [name]) marks;;
- : string list = ["asha"; "ravi"; "meera"; "distinction"; "kiran"]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_session.py
What each function did with the session’s marks:
| Function | What it does | On the session’s data |
|---|---|---|
List.map f l |
applies f to every element |
List.map snd marks gave [72; 35; 91; 58] |
List.filter p l |
keeps the elements for which p is true |
3 of 4 students had 40 or more |
List.partition p l |
splits into kept and dropped, in one pass | m >= 60: 2 pairs and 2 pairs |
List.filter_map f l |
keeps x for every f that returned Some x |
[72; 91; 58] from 5 strings |
List.concat_map f l |
joins the lists f returns |
5 strings from 4 students |
List.filter_map int_of_string_opt read the marks typed as text and skipped "absent" and the empty string in one
pass; a plain List.map gave [Some 72; None; Some 91; None; Some 58], which still needs a filter and an unwrap. Each
of these functions keeps the order of the elements, and none of them changes the list it is given.
One pass for a step that may fail
When a step can fail, as parsing can, use List.filter_map with a function that returns an option, instead of a
List.map followed by a filter for Some and an unwrap. The result has no None left to forget.
The two folds combine from opposite ends
A fold: Combining all the elements of a list into one value with a function and a start value; List.fold_left starts from the first element and List.fold_right from the last. combines all the elements of a list into one value, with a function that takes the
result so far and one element. The manual defines List.fold_left f init [b1; …; bn] as
f (… (f (f init b1) b2) …) bn, which starts from the first element, and List.fold_right f [a1; …; an] init as
f a1 (f a2 (… (f an init) …)), which starts from the last. Step through both on [1; 2; 3] with subtraction:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Two folds, one list. Both folds get the list [1; 2; 3], the start value 10 and subtraction. fold_left begins with the first element; fold_right must first walk to the last one.
- The first subtraction. fold_left computes 10 - 1 = 9 and moves on. fold_right starts at the end: 3 - 10 = -7, with the element on the left and the result so far on the right.
- The second subtraction. fold_left: 9 - 2 = 7. fold_right: 2 - (-7) = 9. Each fold feeds its own running result into the next step.
- Two different answers. fold_left ends with 7 - 3 = 4 and fold_right with 1 - 9 = -8, the values the session printed. Subtraction depends on the order, so the direction of the fold decides the result.
- Why the stack differs. fold_left carried its result forward, so each step was a tail call in one frame. fold_right kept a frame waiting for every element until it reached the end, one per element of the list.
from session import toplevel
# fold_left combines from the first element, fold_right from the last. With an operator that is not associative, such
# as subtraction, the two give different results; with :: one reverses the list and the other copies it.
toplevel("""
List.fold_left ( - ) 10 [1; 2; 3];;
List.fold_right ( - ) [1; 2; 3] 10;;
List.fold_left ( + ) 0 [1; 2; 3];;
List.fold_left (fun acc x -> x :: acc) [] [1; 2; 3];;
List.fold_right (fun x acc -> x :: acc) [1; 2; 3] [];;
(List.fold_left ( ^ ) "" ["a"; "b"; "c"], List.fold_right ( ^ ) ["a"; "b"; "c"] "");;
List.fold_left (fun (lo, hi) x -> (min lo x, max hi x)) (max_int, min_int) [72; 35; 91; 58];;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# List.fold_left ( - ) 10 [1; 2; 3];;
- : int = 4
# List.fold_right ( - ) [1; 2; 3] 10;;
- : int = -8
# List.fold_left ( + ) 0 [1; 2; 3];;
- : int = 6
# List.fold_left (fun acc x -> x :: acc) [] [1; 2; 3];;
- : int list = [3; 2; 1]
# List.fold_right (fun x acc -> x :: acc) [1; 2; 3] [];;
- : int list = [1; 2; 3]
# (List.fold_left ( ^ ) "" ["a"; "b"; "c"], List.fold_right ( ^ ) ["a"; "b"; "c"] "");;
- : string * string = ("abc", "abc")
# List.fold_left (fun (lo, hi) x -> (min lo x, max hi x)) (max_int, min_int) [72; 35; 91; 58];;
- : int * int = (35, 91)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fold_session.py
With ( - ) the answers were 4 and -8, because subtraction depends on the order. With ( + ) the order cannot
matter, and with ( ^ ) both folds gave "abc", because joining strings is associative. Folding :: shows the
directions most plainly: fold_left built [3; 2; 1], a reversed list, while fold_right rebuilt [1; 2; 3]. The
last line carried a pair, the lowest and highest mark so far, and gave (35, 91) in one pass.
The two also differ in stack use. fold_left hands its result forward in a tail call, so it runs in one frame;
fold_right cannot combine anything until it has reached the end, and keeps a frame waiting for every element. The
manual says so in the documentation of fold_right: “Not tail-recursive”. The program below folds a million numbers
both ways under a stack limited to 100,000 words:
from session import run, write
# fold_right must reach the end of the list before it can combine anything, so it keeps one frame per element;
# fold_left combines as it goes, in constant stack. Under a small stack limit only fold_right overflows.
write("folds.ml", """let () =
let l = List.init 1_000_000 (fun i -> i mod 10) in
let total =
match Sys.argv.(1) with
| "left" -> List.fold_left ( + ) 0 l
| _ -> List.fold_right ( + ) l 0
in
Printf.printf "fold_%s over %d elements: %d\\n" Sys.argv.(1) (List.length l) total
""")
run("""
ocamlopt folds.ml -o folds
export OCAMLRUNPARAM=l=100000
./folds left
!./folds right
""") Output
$ ocamlopt folds.ml -o folds $ export OCAMLRUNPARAM=l=100000 $ ./folds left fold_left over 1000000 elements: 4500000 $ ./folds right 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 stack_session.py
fold_left printed 4500000, and fold_right stopped with “Fatal error: exception Stack_overflow” and exit status 2.
Key idea
fold_left combines from the first element in constant stack; fold_right combines from the last and needs a
frame per element. Pick the one whose order your function needs, and fold_left for long lists.
Checkpoint
Check: map, filter and their relatives · fold_left and fold_right
Map and filter are folds, and two laws merge passes
A fold can rebuild a list, so map and filter are folds whose step adds an element in front of the result so far.
fold_right is the natural choice, because it meets the elements from the last one and so builds the new list in
the right order. The session writes both in one line each, then checks two laws on the list [3; 8; 1; 6; 4]:
from session import run, toplevel, write
# map and filter are folds that rebuild a list. Two laws hold for any pure functions: two maps equal one map of the
# composed function, and two filters equal one filter of both tests. The fused form builds one list instead of two.
toplevel("""
let map f l = List.fold_right (fun x acc -> f x :: acc) l [];;
let filter p l = List.fold_right (fun x acc -> if p x then x :: acc else acc) l [];;
let l = [3; 8; 1; 6; 4];;
(map (fun x -> x * 2) l, filter (fun x -> x > 3) l);;
(map (fun x -> x * 2) l = List.map (fun x -> x * 2) l, filter (fun x -> x > 3) l = List.filter (fun x -> x > 3) l);;
let f x = x + 1 and g x = x * 10;;
List.map f (List.map g l) = List.map (fun x -> f (g x)) l;;
let p x = x mod 2 = 0 and q x = x > 3;;
List.filter p (List.filter q l) = List.filter (fun x -> q x && p x) l;;
List.filter (fun y -> y > 20) (List.map g l) = List.filter_map (fun x -> let y = g x in if y > 20 then Some y else None) l;;
""")
write("fused.ml", """let words f =
let before = Gc.minor_words () in
let r = f () in
(r, Gc.minor_words () -. before)
let () =
let l = List.init 1000 Fun.id in
let f x = x + 1 and g x = x * 10 in
let a, w2 = words (fun () -> List.map f (List.map g l)) in
let b, w1 = words (fun () -> List.map (fun x -> f (g x)) l) in
Printf.printf "same result: %b\\n" (a = b);
Printf.printf "two maps: %.0f words, one fused map: %.0f words\\n" w2 w1
""")
run("""
ocamlopt fused.ml -o fused
./fused
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let map f l = List.fold_right (fun x acc -> f x :: acc) l [];;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>
# let filter p l = List.fold_right (fun x acc -> if p x then x :: acc else acc) l [];;
val filter : ('a -> bool) -> 'a list -> 'a list = <fun>
# let l = [3; 8; 1; 6; 4];;
val l : int list = [3; 8; 1; 6; 4]
# (map (fun x -> x * 2) l, filter (fun x -> x > 3) l);;
- : int list * int list = ([6; 16; 2; 12; 8], [8; 6; 4])
# (map (fun x -> x * 2) l = List.map (fun x -> x * 2) l, filter (fun x -> x > 3) l = List.filter (fun x -> x > 3) l);;
- : bool * bool = (true, true)
# let f x = x + 1 and g x = x * 10;;
val f : int -> int = <fun>
val g : int -> int = <fun>
# List.map f (List.map g l) = List.map (fun x -> f (g x)) l;;
- : bool = true
# let p x = x mod 2 = 0 and q x = x > 3;;
val p : int -> bool = <fun>
val q : int -> bool = <fun>
# List.filter p (List.filter q l) = List.filter (fun x -> q x && p x) l;;
- : bool = true
# List.filter (fun y -> y > 20) (List.map g l) = List.filter_map (fun x -> let y = g x in if y > 20 then Some y else None) l;;
- : bool = true
$ ocamlopt fused.ml -o fused
$ ./fused
same result: true
two maps: 6000 words, one fused map: 3000 words
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fusion_session.py
The home-made map and filter gave [6; 16; 2; 12; 8] and [8; 6; 4], the same lists as List.map and
List.filter. The first law, map fusion, says that mapping g and then f equals one map of the function that
applies both: List.map f (List.map g l) = List.map (fun x -> f (g x)) l printed true. The second law joins two
filters into one test, List.filter p (List.filter q l) = List.filter (fun x -> q x && p x) l, also true, and the
last line turned a map followed by a filter into one List.filter_map, true again.
Laws like these let you rewrite a pipeline without changing its answer, as long as f, g, p and q are pure: no
printing, no refs, no exceptions. The compiled program measured what fusion saves on 1,000 elements: two maps
allocated 6,000 words, since the list in the middle is built and thrown away, and the fused map 3,000.
The three rewrites the session checked, side by side:
| Two passes | One pass with the same result |
|---|---|
List.map f (List.map g l) |
List.map (fun x -> f (g x)) l |
List.filter p (List.filter q l) |
List.filter (fun x -> q x && p x) l |
List.filter p (List.map f l) |
List.filter_map (fun x -> let y = f x in if p y then Some y else None) l |
Fusing functions that have effects
Wrong: turning List.map save (List.map parse lines) into one map, where save writes a file and parse may raise.
In two passes a bad line stops the program before anything is saved; fused, the lines before it may already be
saved. Right: fuse only pure functions, or choose the new order on purpose.
A pipeline lists the steps in the order the data takes them
x |> f is f x, so a chain of |> reads like a list of instructions: start from this data, keep these elements,
take this part, combine. The session totals the passing marks both ways, as a pipeline and as nested calls. Step
through the pipeline stage by stage:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The data. marks pairs each name with a mark: asha 72, ravi 35, meera 91 and kiran 58. The pipeline starts from this list.
- Keep the passes. List.filter keeps the pairs whose mark is 40 or more. Ravi's 35 is dropped, so three pairs go on, in their original order.
- Keep only the marks. List.map snd replaces each pair with its second part, the mark, which gives [72; 91; 58].
- Combine into one number. List.fold_left ( + ) 0 adds the marks from the left: 0 + 72 + 91 + 58 = 221, the total the session printed.
- Reading order. Each |> hands the value on its left to the function on its right, so the steps read top to bottom in the order they run. The nested call gave the same 221 written inside out.
from session import toplevel
# x |> f is f x, so a chain of |> lists the steps in the order the data goes through them. The nested form gives the
# same result written inside out.
toplevel("""
let marks = [("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)];;
marks |> List.filter (fun (_, m) -> m >= 40) |> List.map snd |> List.fold_left ( + ) 0;;
List.fold_left ( + ) 0 (List.map snd (List.filter (fun (_, m) -> m >= 40) marks));;
let passed = List.filter (fun (_, m) -> m >= 40) marks;;
List.fold_left ( + ) 0 (List.map snd passed) / List.length passed;;
marks |> List.sort (fun (_, a) (_, b) -> compare b a) |> List.map fst |> List.take 2;;
""") Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let marks = [("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)];;
val marks : (string * int) list =
[("asha", 72); ("ravi", 35); ("meera", 91); ("kiran", 58)]
# marks |> List.filter (fun (_, m) -> m >= 40) |> List.map snd |> List.fold_left ( + ) 0;;
- : int = 221
# List.fold_left ( + ) 0 (List.map snd (List.filter (fun (_, m) -> m >= 40) marks));;
- : int = 221
# let passed = List.filter (fun (_, m) -> m >= 40) marks;;
val passed : (string * int) list =
[("asha", 72); ("meera", 91); ("kiran", 58)]
# List.fold_left ( + ) 0 (List.map snd passed) / List.length passed;;
- : int = 73
# marks |> List.sort (fun (_, a) (_, b) -> compare b a) |> List.map fst |> List.take 2;;
- : string list = ["meera"; "asha"]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipeline_session.py
Both forms gave 221, but the nested call reads from the innermost function outwards, the opposite of the order in
which the steps run. The average of the three passing marks was 73, because / on int drops the fraction of
221 / 3. Sorting by mark from the highest, keeping the names and taking two with List.take, new in OCaml 5.3, gave
["meera"; "asha"]: three stages after the data, each on its own line in a real program, each easy to test on its
own.
Key idea
Write the steps of a list computation as a |> pipeline of map, filter and fold: it reads in the order it runs, and
each stage is a small function you can test.
Checkpoint
Check: Map and filter as folds, fusion · Pipelines with |>
Practise: map, filter, fold and pipelines
The questions use the session’s marks, folds and laws with new lists. Work out each result, or decide whether two forms are equal, 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 (fresher to 2–5 years): how do fold_left and fold_right differ in OCaml?
Both combine the elements of a list into one value with a function and a start value. fold_left starts from the
first element, f (f (f init a) b) c, and passes its result forward in a tail call, so it runs in constant stack
space. fold_right starts from the last element, f a (f b (f c init)), and needs one stack frame per element, which
the standard library documents as not tail-recursive. With an operator that depends on order the results differ:
subtraction from 10 over [1; 2; 3] gives 4 from the left and -8 from the right. Folding :: from the left reverses
a list, and from the right rebuilds it in order.
Two harder questions follow, about a total that overflows on a large import and about a parsing pipeline that keeps options around too long. For each, name the pass or the fold you would change 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 data import totals its amounts with List.fold_right (fun x acc -> x.amount + acc) rows 0. It works on every test file, but a customer's file of 3 million rows stops it with Stack_overflow. Explain why, fix it, and say how you would keep the fix from regressing.
-
Fresher to mid
A pipeline parses lines with lines |> List.map parse |> List.filter Option.is_some |> List.map Option.get, where parse returns an option. It works, but a teammate calls it fragile and slow. Rewrite it, and explain what each change improves.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which List functions replace the usual hand-written recursions over a list?
map, filter, partition, filter_map and concat_map. They keep the order of the elements and never change the list they are given.
-
When is List.filter_map better than a map followed by a filter?
When a step can fail, as parsing can: the function returns an option, and the result has no None left to forget.
-
What do List.fold_left ( - ) 10 [1; 2; 3] and List.fold_right ( - ) [1; 2; 3] 10 give?
4 and -8. fold_left combines from the first element, fold_right from the last, so an order-sensitive function gives two answers.
-
Which fold suits a list of a million elements, and why?
fold_left: its recursive call is a tail call, so it needs constant stack space, while fold_right needs one stack frame per element.
-
When can two List.map passes be fused into one?
When both functions are pure: one pass then gives the same list without building one in the middle. With effects, such as saving after a parse that may raise, fusing changes what happens before a failure.
-
Why write a list computation as a |> pipeline of map, filter and fold?
It lists the steps in the order the data takes them, each stage is a small testable function, and the result equals the nested calls.
Key takeaways
List.map,filter,partition,filter_mapandconcat_mapreplace the usual recursions over a list, keep the order of the elements and never change the list they are given.fold_leftcombines from the first element in constant stack, whilefold_rightcombines from the last and needs a frame per element, so the two give different answers for subtraction and different stack use for long lists.mapandfilterare folds that rebuild a list, and for pure functions two maps or two filters can be fused into one pass that builds no list in the middle.- A
|>pipeline of map, filter and fold lists the steps in the order they run, and gives the same result as the nested calls.
Terms in this lesson
- fold
- Combining all the elements of a list into one value with a function and a start value; List.fold_left starts from the first element and List.fold_right from the last.
- higher-order function
- A function that takes another function as an argument or returns one, such as List.map, which applies the function it is given to every element of a list.
References
- The OCaml Manual: module List (Inria and the OCaml developers)
- Higher-Order Functions (OCaml.org)
- Lists (OCaml.org)
- The OCaml Manual: The runtime system, OCAMLRUNPARAM (Inria and the OCaml developers)
Report a problem with this lesson
Kept only in this browser. Your Learn progress