Home > Enterprise >  Understanding foldr and foldl functions
Understanding foldr and foldl functions

Time:05-17

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f v [] = v
foldr f v (x:xs) = f x (foldr f v xs)
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v [] = v
foldl f v (x:xs) = foldl f (f v x) xs

I am trying to wrap my head around this two functions. I have two questions. One regarding function f. In general,

foldr f v xs

f has access to the first element of xs and the recursively processed tail. Here:

foldl f v xs

f has access to the last element of xs and the recursively processed tail.

Is this an useful (and correct) way to think about it ?

My second question is related to fold "right" or "left". In many places, they say that foldr "starts from the right". For example, if I expand the expression

foldr ( ) 0 [1,2,3]

I get

( ) 1 (foldr ( ) 0 [2,3])

So, I see it is "starting from the left" of the list. The first element and the recursively processed tail are the arguments to the function. Could someone give some light into this issue ?

EDIT: One of my question focuses is on the function f passed to fold; the linked answer doesn't address that point.

CodePudding user response:

"Starting from the right" is good basic intuition, but it can also mislead, as you've already just discovered. The truth of the matter is that lists in Haskell are singly linked and we only have access to one side directly, so in some sense every list operation in Haskell "starts" from the left. But what it does from there is what's important. Let's finish expanding your foldr example.

foldr ( ) 0 [1, 2, 3]
1   foldr 0 [2, 3]
1   (2   foldr 0 [3])
1   (2   (3   foldr 0 []))
1   (2   (3   0))

Now the same for foldl.

foldl ( ) 0 [1, 2, 3]
foldl ( ) (0   1) [2, 3]
foldl ( ) ((0   1)   2) [3]
foldl ( ) (((0   1)   2)   3) []
((0   1)   2)   3

In the foldr case, we make our recursive call directly, so we take the head and make it an argument to our accumulating function, and then we make the other argument our recursive call.

In the foldl case, we make our recursive call by changing the the accumulator argument. Since we're changing the argument rather than the result, the order of evaluation gets flipped around.

The difference is in the way the parentheses "associate". In the foldr case, the parentheses associate to the right, while in the foldl case they associate to the left. Likewise, the "initial" value is on the right for foldr and on the left for foldl.

The general advice for the use of folds on lists in Haskell is this.

  • Use foldr if you want lazy evaluation that respects the list structure. Since foldr does its recursion inside the function call, so if the folding function happens to be guarded (i.e. by a data constructor), then our foldr call is guarded. For instance, we can use foldr to efficiently construct an infinite list out of another infinite list.

  • Use foldl' (note the ' at the end), the strict left fold, for situations where you want the operation to be strict. foldl' forces each step of the fold to weak head normal form before continuing, preventing thunks from building up. So whereas foldl will build up the entire internal expression and then potentially evaluate it at the end, foldl' will do the work as we go, which saves a ton of memory on large lists.

  • Don't use foldl on lists. The laziness gained by foldl is almost never useful, since the only way to get anything useful out of a left fold is to force the whole fold anyway, building up the thunks internally is not useful.

For other data structures which are not right-biased, the rules may be different. All of this is running on the assumption that your Foldable is a Haskell list.

  • Related