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. Sincefoldr
does its recursion inside the function call, so if the folding function happens to be guarded (i.e. by a data constructor), then ourfoldr
call is guarded. For instance, we can usefoldr
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 whereasfoldl
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 byfoldl
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.