fold: reduce a list to a single value
map returns a list.
filter returns a list. fold returns
anything: a number, a string, a record, another list, a tree. If
the answer to your problem is computed by walking the elements of a
list and combining them somehow, fold is the tool. It is the most
general of the three canonical higher-order list functions, and it
subsumes both map and filter: as we will see by the end of the
lecture, you can write both of them on top of fold.
This lecture follows the same shape as the previous two: start from
two concrete recursive functions that share a pattern, abstract the
pattern, name the abstraction, then study the two flavours that
arise (fold_left and fold_right). At the end we look at folds
beyond lists.
From sum and all_true to fold
Two functions:
Same shape, two differences:
- The base case returns a different value:
0forsum,trueforall_true. - The combining step uses a different operator:
+forsum,&&forall_true.
Both differences need to be parameterised. The base case becomes an
argument we will call init (the initial accumulator); the operator
becomes a function we will call f. Putting them together:
That little function reduce is, with a tiny renaming, the
standard library function List.fold_right. The renaming: people
conventionally write acc instead of init for the accumulator
argument, and put the list before the accumulator. So:
That is the actual signature of List.fold_right. Read the type:
val fold_right : ('a -> 'acc -> 'acc) -> 'a list -> 'acc -> 'acc
f takes an element and an accumulator, produces a new accumulator.
The function takes a list of 'a, an initial accumulator of type
'acc, and returns the final accumulator.
What fold_right computes
The name "fold right" comes from how the operator gets associated.
For fold_right f [a; b; c] init, the computation unfolds as:
f a (f b (f c init))
That is, the rightmost element is combined first with the initial accumulator, then that result with the next element, and so on inward. The parentheses associate to the right. (The recursion walks the list left-to-right to reach the empty tail, but the arithmetic happens on the way back, rightmost element first.)
A useful way to picture this: a list value is a chain of cons
cells terminated by []. The fold replaces every cell in that
chain: each :: becomes a call to f, and the terminal []
becomes init. A four-element list [x1; x2; x3; x4] is built
by the chain x1 :: x2 :: x3 :: x4 :: [], and folding right with
(+) and 0 produces x1 + x2 + x3 + x4 + 0. The fold reuses
the list's own structure as the skeleton of the computation.
That is also why this signature is so general: it lets you replace
the list's "structure" with any operator and any initial value you
like. Pick + and 0: you get a sum. Pick ^ and "": you get a
concatenation. Pick :: and []: you get back the original list
(because we are replacing :: with itself and [] with itself).
Pick (fun x acc -> 1 + acc) and 0: you get the length. Pick
(fun x acc -> f x :: acc) and [] for some function f: you get
map. Pick (fun x acc -> if p x then x :: acc else acc) and []:
you get filter. Folds are very general.
fold_left: the other direction
There is a second flavour of fold, called fold_left, that combines
elements from the left instead of the right. Where fold_right
parenthesises rightward (f a (f b (f c init))), fold_left
parenthesises leftward (f (f (f init a) b) c).
There are two differences from fold_right:
-
The argument order of the combining function is swapped. In
fold_right,ftakeselementthenaccumulator. Infold_left,ftakesaccumulatorthenelement. Mnemonic: the accumulator goes on the side suggested by the name.fold_Xhas accumulator on theX. -
The list argument is in a different position. In
fold_rightyou writefold_right f xs init; infold_leftyou writefold_left f init xs. The accumulator comes before the list. This is a small inconsistency in the standard library, but it has been the convention since the early days of OCaml.
A side-by-side reminder, because the order trips up almost everyone the first ten times:
combining function f |
call shape | |
|---|---|---|
fold_right |
'a -> 'acc -> 'acc (element, acc) |
fold_right f xs init |
fold_left |
'acc -> 'a -> 'acc (acc, element) |
fold_left f init xs |
Mnemonic: in fold_X, the accumulator sits on the X side, both
inside f and in the call.
What fold_left computes, step by step
For fold_left f acc [x1; x2; x3], the computation is:
f (f (f acc x1) x2) x3
Read inside-out: apply f to the initial acc and x1; take that
result and apply f to it and x2; take that result and apply
f to it and x3. The accumulator threads through, getting
updated by each element in turn.
For fold_left (+) 0 [1; 2; 3], the unfolding is ((0 + 1) + 2) + 3, which is 6. For addition, this gives the same answer as
fold_right: both produce 6. That is because + is associative
(the same result regardless of how you parenthesise) and the initial
accumulator 0 is the identity (adding 0 does not change the
result).
For a non-associative operator, the two folds disagree. Subtraction is the textbook example:
Same list, same operator, different answers. When this happens, you have to pick the fold direction that matches the meaning you want.
fold_left is tail-recursive
The other big difference between fold_left and fold_right is
where the recursive call sits.
The recursive call to fold_left is in the tail position: nothing
happens after it returns. OCaml will compile this to a loop. The
function uses constant stack space regardless of list length: you
can fold a list of millions of elements without trouble.
Contrast with fold_right:
The recursive call is inside f h (...): after it returns, we
still have to apply f to its result and h. So the recursive call
is not in tail position;
each pending call lives on the stack until its callee returns. For a
list of n elements, fold_right builds a stack of depth n. For
lists in the millions, this overflows.
So when do you reach for which?
- For tail-recursion and constant stack, use
fold_left. - When the natural order is right-to-left (because the operation is
not associative, and the right grouping matches your meaning), use
fold_right. If the list is short, do not worry. If the list is very long, useList.revand switch tofold_left. - A useful identity:
fold_right f xs init = fold_left (fun acc x -> f x acc) init (List.rev xs). The standard library'sfold_rightstays plainly recursive, but third-party libraries (Containers, Base) use this rev-then-fold_lefttrick for stack-safe right folds, and you can apply it yourself when the list is long.
In day-to-day OCaml, fold_left is by far the more common choice.
Implementing map and filter via fold
We claimed map and filter can be expressed in terms of fold. The
proofs are short.
For map:
The combining function (fun x acc -> f x :: acc) says: at each
step, apply f to the current element and cons the result onto the
accumulator. With fold_right, the walk goes right-to-left, so the
cons-order matches the original order of the list and we get the
mapped list out.
For filter:
The combining function picks either x :: acc (keep) or acc
(drop). Same general idea: walk the list, build the accumulator,
hand it back at the end.
fold is more general than both map and filter. If you ever
forget the signature of either, you can derive it from fold. (We
will not recommend you write map_via_fold instead of map in real
code: the more specific functions express intent more clearly. But
knowing they are all the same machinery is part of understanding the
toolbox.)
Beyond lists: fold any structure
Fold scales further than it looks. When Google needed ordinary
engineers to process web-scale data across thousands of machines,
the abstraction they reached for was
MapReduce
(2004), named outright after map and reduce (reduce being
fold): map a function over the shards, then fold the partial
results into one answer. The two combinators on this page are
the same two that, scaled out, indexed the web. The reason it
works is that a fold says only how to combine, not in what
order the machine must walk the data.
Fold also generalises to anything recursive. Trees are the next-most-common example:
For a Leaf, return the accumulator unchanged. For a Node (l, v, r), fold the left subtree first, then combine with the value at the
node, then fold the right subtree. The result is an in-order fold:
left subtree, root, right subtree.
The general technique: for any recursive data type, write a fold that takes one function argument per constructor (or per place the type recursively occurs), and at each constructor pass the appropriate combining function. This pattern generalises beyond trees to any algebraic data type, and it is the entry point to the abstract idea of a catamorphism (a fancy name for "generalised fold") that appears in category theory. We will see more of it in Module 8.
When fold is overkill
Folds are powerful, but power has a cost: a non-trivial fold can be
harder to read than the equivalent map/filter chain, because the
reader has to decode the accumulator threading. The rule of thumb:
sum_squares_a first squares each element with map, then folds
with +. It builds an intermediate list of squares. sum_squares_b
does the squaring and accumulation in a single fold, never allocating
the intermediate list. Both produce 14.
For small inputs, prefer the clearer pipeline. For very long inputs or hot loops, the single-fold version may be measurably faster. Profile before "optimising" by fusing operations; readability is worth more than constant factors in most code.
A short subtlety: fold_left arguments and direction
A common confusion: people remember "fold_left = tail-recursive" and
then are surprised when fold_left produces a reversed result
where they wanted the original order.
It returns [3; 2; 1], not [1; 2; 3].
Why? fold_left walks left to right. At each step, we cons the
current element onto the accumulator. After the first element, acc = [1]. After the second, acc = [2; 1] (we prepended 2). After
the third, acc = [3; 2; 1]. So the first element ends up
deepest, and the result is reversed.
This is the same machinery as List.rev, and indeed:
is a standard one-line definition of rev. If you want the original
order, either fold right (List.fold_right (fun x acc -> x :: acc) xs [], which gets [1; 2; 3]) or fold_left then List.rev.
A quick check
What is List.fold_left (+) 0 [1; 2; 3; 4]?
041024
Why: fold_left (+) 0 [1; 2; 3; 4] evaluates to (((0 + 1) + 2) + 3) + 4 = 10. Initial accumulator 0, then +1, +2, +3, +4.
Which of the following is not tail-recursive?
List.fold_leftList.fold_rightList.lengthList.rev
Why: List.fold_right has work to do after the recursive call:
applying f to the element and the recursive result. The other
three are tail-recursive in the standard library (length and rev
use fold_left or accumulator-based traversal internally).
A code challenge:
Express List.length xs using List.fold_left. Do not call
List.length itself, and do not use a let rec.
Show reference solution
Reference solution: `let my_length xs = List.fold_left (fun n _ -> n
-
- 0 xs
. We ignore each element (the_`) and just bump the counter. Tail-recursive, constant stack.
- 0 xs
Activity
Show reference solution
What's next
We have the three big higher-order list functions: map, filter,
fold. Next lecture: the pipeline operator |>,
which lets us chain these together cleanly and read the result
top-to-bottom.
Reading
- Cornell CS3110, Fold: https://cs3110.github.io/textbook/chapters/hop/fold.html
- Real World OCaml, Lists and patterns: https://dev.realworldocaml.org/lists-and-patterns.html
- Graham Hutton, A tutorial on the universality and expressiveness
of fold: a beautifully written paper showing how powerful
foldreally is. Optional but enjoyable. https://www.cs.nott.ac.uk/~pszgmh/fold.pdf
Sources
This lecture's prose, worked examples, and quizzes are original to
this course. Materials referenced during preparation are listed in
the Reading section above; Cornell CS3110 and Real World OCaml
are CC BY-NC-ND-licensed and have not been derivatively reused.
See LICENSES.md
at the repository root for the full source posture.