Differences Between `Foldl` and `Foldr`
Understanding the Distinctions Between foldl and foldr
Editor's Note: This piece is a rewritten version of a foundational guide regarding the differences between
foldrandfoldl(including their strict and lazy variants). Originally appearing in a GitHub pull request forhasura/graphql-engineon September 26, 2019, this explanation has become a staple for onboarding newcomers. We are grateful to Alexis King for permitting its publication here.
The Fundamental Misconception
To begin, we must clear up a common misunderstanding: foldl and foldr are not "folds from the left" and "folds from the right." In reality, both functions traverse the data structure in the exact same direction. For lists, this means they both move from the head to the tail (left to right).
The actual difference lies in the grouping (associativity) of the operations.
Visualizing the Association
If we have a list , a starting value , and a combining function , the two folds expand as follows:
1. foldl (Left-Associated):
2. foldr (Right-Associated):
Behavior in Strict Languages
In a strict (eager) language, expressions are evaluated from the inside out. The most deeply nested operation is resolved first.
Consider the expression foldl (+) 0 [1, 2, 3, 4]. It expands to:
(((0 + 1) + 2) + 3) + 4
The reduction sequence looks like this:
| Step | Expression | Result |
|---|---|---|
| 1 | (((0 + 1) + 2) + 3) + 4 | ((1 + 2) + 3) + 4 |
| 2 | ((1 + 2) + 3) + 4 | (3 + 3) + 4 |
| 3 | (3 + 3) + 4 | 6 + 4 |
| 4 | 6 + 4 | 10 |
If we used foldr (+) 0 [1, 2, 3, 4], the expansion is 1 + (2 + (3 + (4 + 0))). While the result is the same (because addition is commutative and associative), the evaluation order is reversed.
The Practical Impact: Space Complexity
foldl: To begin reducing, it only needs the first element. This allows it to be implemented as a tail-recursive loop, operating in constant space.foldr: It must reach the very end of the list before the first addition (4 + 0) can occur. For a list of size , the program must push frames onto the call stack. This is why people mistakenly call it "folding from the right."
Behavior in Lazy Languages (Haskell)
In lazy languages, the rules change. Evaluation happens from the outside in, triggered only when a value is actually demanded.
The Problem with Lazy foldl
In Haskell, foldl (+) 0 [1, 2, 3, 4] does not immediately execute a loop. Instead, it builds a massive chain of suspended computations called thunks. If we represent thunks with ⟨ ⟩, the expression becomes:
⟨⟨⟨⟨ 0 + 1 ⟩ + 2 ⟩ + 3 ⟩ + 4 ⟩
Because (+) is strict in both arguments, the outermost thunk cannot be resolved until the innermost one is. This creates a linear-space thunk tree.
- The Danger: In a strict language, the list itself takes linear space. In a lazy language, lists can be streams (constant space). By using lazy
foldl, you might accidentally turn a constant-space stream process into a linear-space memory hog.
The Solution: foldl'
To fix this, Haskell provides foldl', the strict version. It forces the evaluation of the accumulator at each step.
- Instead of building
⟨⟨0 + 1⟩ + 2⟩, it evaluates0 + 1immediately to get1, then builds⟨1 + 2⟩. - This restores the constant space efficiency of strict languages.
Lazy foldr and Strict Operations
Even in a lazy language, if you use foldr with a strict operation like (+), you still face linear space requirements.
Looking at the expansion:
foldr (+) 0 [1, 2, 3, 4] = 1 + (2 + (3 + (4 + 0)))
To compute the final sum, the program must still traverse to the end of the list to find the (4 + 0) base case before it can start collapsing the expression.
Summary Checklist
- Traversal Order: Both
foldlandfoldrmove left-to-right. - Associativity:
foldlis left-associated;foldris right-associated. - Strict Languages:
foldlis efficient (tail-recursive);foldruses stack space. - Lazy Languages:
foldlis dangerous (builds thunks).foldl'is the preferred strict alternative for efficiency.foldris necessary for certain lazy patterns (though linear space for strict ops).