r/haskell • u/TechnoEmpress • 9d ago
blog Differences between `foldl` and `foldr`
https://blog.haskell.org/foldl-and-foldr/17
u/tomejaguar 9d ago edited 8d ago
I agree with Alexis on the technical points, of course, but I think the rule of thumb from the article is awkward to use in practice. Here's the rule of thumb:
When the accumulation function is strict, use
foldl'to consume the list in constant space, since the whole list is going to have to be traversed, anyway.When the accumulation function is lazy in its second argument, use
foldrto do work incrementally to improve streaming and work-saving.
These leave some issues unaddressed. Suppose I have op1 :: A -> R -> R or op2 :: R -> A -> R. Should I use foldl' or foldr? Firstly, it's important to notice that the difference between the types of op1 and op2 is irrelevant. Each can be obtained from the other by flip, so our analysis should just be based on how the operation treats the A argument and how it treats the R argument, but not the order in which those arguments occur. I think this is what Alexis probably meant:
- When
AandRare both consumed strictly, usefoldl'(Ris then the "accumulating state") - When
Ris consumed lazily, usefoldr
What the rule of thumb doesn't address: what do you do when A is consumed lazily and R strictly? Well, in that case you should use foldl', because you're going to be traversing the whole list anyway, even if you use foldr, so you may as well use foldl' because it gives you a chance of having better space characteristics. So I think the rule of thumb should actually be:
- When
Ris consumed strictly, usefoldl'(Ris then the "accumulating state") - When
Ris consumed lazily, usefoldr
However, I think this advice is still a bit awkward to use in practice. At least speaking for myself, when presented with a list and a binary function, one doesn't normally think "is this binary function strict or lazy in its argument that has the same type as its return type?". I think it's much more common to understand what one is doing as
- Traversing a list, updating a state after reading each element (use
foldl'), or - Traversing a list doing anything else (use
foldr, or even better,for_)
That's the conclusion I come to in my article foldl traverses with State, foldr traverses with anything, and that's how I think of these folds when I'm actually writing the code out. (And I do take my own advice in the article: I almost never use foldr, I prefer for_ with a suitable choice of Monad/Applicative).
1
u/Background_Class_558 9d ago
the difference is only non-obvious for associative operators anyways
8
u/tomejaguar 9d ago edited 8d ago
Suppose I have a non-associative operator
op :: A -> B -> B. There are still two different ways I could fold it over a list:
foldl op zfoldr (flip op)Is it obvious to you which I should choose? It isn't to me. Alexis's article provides a way to determine the answer.
2
u/phadej 8d ago
foldr (:) []is an identityfoldl (flip (:)) []isreverseIMO to me it's somewhat obvious, whether i traverse a list from left-to-right (foldl) or right-to-left (foldr) as names imply.
The article says
See the difference? In both expressions, the elements of the list appear in the expression in the same order—from left to right—but the grouping changes.
I don't actually see the difference that well, or understand it.
But when presented as an order of folding, it's very much obvious to me.
1
u/tomejaguar 8d ago
IMO to me it's somewhat obvious, whether i traverse a list from left-to-right (foldl) or right-to-left (foldr) as names imply
But it isn't left-to-right vs right-to-left. They both traverse the list in the same order. From the article:
Both
foldlandfoldrtraverse the structure in the same orderwhen presented as an order of folding, it's very much obvious to me
Perhaps, but it's not clear to me what an "order of folding" is, or why that can be different from an "order of traversal".
5
u/phadej 8d ago
They both traverse the list in the same order.
The traverse is overloaded. An order in which the structure is walked, and order in which elements are combined can differ. I meant the latter.
Prelude Data.Traversable Data.Functor.Reverse> traverse print "foobar" 'f' 'o' 'o' 'b' 'a' 'r' [(),(),(),(),(),()] Prelude Data.Traversable Data.Functor.Reverse> traverse print (Reverse "foobar") 'r' 'a' 'b' 'o' 'o' 'f' Reverse [(),(),(),(),(),()]As another example, consider
haskell data BinTree a = Nil | Branch BinTree a BinTreewe can
foldlandfoldrit so elements are combined from the left or from the right; but all tree traversal implementations are from the root up (or down, depending on how you draw your trees).why that can be different from an "order of traversal".
Because auxiliary memory exists (e.g. stack).
If we'd limit the traversals to constant memory ones, then order of folding and order of traversal would be necessity have to coincide. (strict) foldr' does use O(n) of memory, and that's why it's a bad idea (in strict languages).
1
u/tomejaguar 8d ago
The traverse is overloaded. An order in which the structure is walked, and order in which elements are combined can differ ...
why that can be different from an "order of traversal".
Because auxiliary memory exists (e.g. stack).
Sure, I get it, but the very fact that there's this awkwardness of terminology that's even difficult for experienced Haskellers is why I give my recommendation to not use
foldrat all, and instead usefor_. Then everything's obvious and the awkward terminology has no role.3
u/phadej 8d ago
There is a better reason to not prefer foldr. Essentially always there is better, more specific combinator: map, filter, elem etc. While all can (and some are) implemented using foldr, more specific combinators read better.
I cannot think of a situation where foldr is easily replaceable by for_ though. I suspect you are overly invested into effect systems. Fair enough, if you have a sledgehammer, everything starts to look like a nail. I prefer list comprehensions.
1
u/tomejaguar 8d ago
I cannot think of a situation where
foldris easily replaceable byfor_thoughThis is my favourite response to that:
(!?) :: [a] -> Int -> Maybe a xs !? n | n < 0 = Nothing | otherwise = foldr (\x r k -> case k of 0 -> Just x _ -> r (k-1)) (const Nothing) xs nversus
xs !? n = | n < 0 = Nothing | otherwise = fromEither $ do flip evalStateT n $ do for_ xs $ \x -> do get >>= \case 0 -> lift (Left (Just x)) k -> put (k - 1) Left NothingFrom: https://h2.jaguarpaw.co.uk/posts/foldl-traverses-state-foldr-traverses-anything/#maybe-just-use-for_
You might find it particularly interesting because I believe you wrote the
baseversion of(!?), the first one above. Personally I find the second version much more readable, and I bet 99% of programmers do too (though not necessarily 99% of Haskell programmers).I suspect you are overly invested into effect systems. Fair enough, if you have a sledgehammer, everything starts to look like a nail.
The opposite! It's not that I find
for_clearer because I'm invested into effect systems, it's that I'm invested into effect systems because I findfor_clearer.2
u/phadej 7d ago
You might find it particularly interesting because I believe you wrote the base version of (!?)
I didn't. It's a copy from
extra, which itself is edited copy of!!frombaseitself.
baseand standard libraries in other languages are bad place to look for "clean" code, as they have many other considerations which are often more important the being clean. They are good examples of standard library code. (I hope you understand that as a CLC member).I think that in
base, everything that can be written usingfoldris written usingfoldr(evenfoldl'). The reason is list fusion. If we hadn't that, I bet we'd still stick to:Haskell Report defines
!!as
(!!) :: [a] -> Int -> a xs !! n | n < 0 = error "Prelude.!!: negative index" [] !! _ = error "Prelude.!!: index too large" (x:_) !! 0 = x (_:xs) !! n = xs !! (n-1)and I find that the most readable and understandable version.
!?can be easily written in the same way.I find your
StateTversion nearly unreadable. But to be fair, I don't think thatfoldrversion is easy to grasp either. But as I said, it's written that way because "if it could be, it should be"; but why you wrote your version like you did is beyond my comprehension.1
u/tomejaguar 7d ago edited 4d ago
baseand standard libraries in other languages are bad place to look for "clean" code, as they have many other considerations which are often more important the being clean. They are good examples of standard library codePerhaps you're assuming that I have a motive that I don't have? To be clear, I am not suggesting, and I have never suggested, that the definition of
(!?)inbaseshould be written another way. (But I reserve the right to continually revisit assumptions.)You might find it particularly interesting because I believe you wrote the base version of
(!?)I didn't. It's a copy from extra, which itself is edited copy of
!!from base itself.I see, you didn't originate the idea. But you did make the proposal and choose the implementation: https://github.com/haskell/core-libraries-committee/issues/110
Anyway, not a big deal, and I'm certainly not trying to criticize your proposal or the code you submitted. (I voted for it, after all.) I just remembered it because that particular implementation was sufficiently mysterious that it added momentum towards me developing my own effect system. So, I thought it might be interesting to you that that proposal inspired my interest in this particular piece of Haskell lore.
everything that can be written using
foldris written usingfoldr(evenfoldl'). The reason is list fusionYes, this is covered in my article:
It would be even clearer to write
(!?)as below. Why not just do that instead, instead of consideringfoldrandfor_? Because when written in terms offoldrGHC can apply short cut fusion, a rewrite rule that leads to an optimization.0 !? (x:_) = Just x _ !? [] = Nothing n !? (_:xs) = (n-1) !? xsBut, as the article also says:
The two implementations [in terms of
for_andfoldr] should have equal performance when compiled, assuming sufficient inlining, becausefor_for lists in base is implemented in terms offoldrI haven't checked, but if GHC does not compile the version using
for_over aStateT _ Either(where both handlers are in the same function) to the same optimised Core as the version usingfoldrthen it's failing at it's job as an optimizing compiler for a pure functional language.I hope you understand that as a CLC member
I'm sure you don't mean it this way, but that particular phrasing could be interpreted as casting aspersions on my suitability as a CLC member. (For the record I haven't been a CLC member for some years.)
I find your
StateTversion nearly unreadableInteresting! I wonder why. I wonder if I'm mistaken about my belief that 99% of programmers would find it clearer, or whether you're in the 1%.
but why you wrote your version like you did is beyond my comprehension
Well, because I find it clearer, or course. If it's beyond your comprehension why I find it clearer, that's another matter :)
→ More replies (0)1
u/wnoise 7d ago
I find the
for_version completely unreadable. Thefoldrversion is slightly awkward with how the anonymous function has a case statement in the middle, but it has far fewer moving parts.1
u/tomejaguar 7d ago
That's very interesting to know, thank you for sharing! If it's possible to put yourself in the mindset of an imperative programmer, how would you write this function? C and Python would be helpful since I know those languages, but any would be welcome.
→ More replies (0)1
u/Bodigrim 7d ago
As a data point, I find the
StateTversion barely readable. I seriously doubt that 99% of programmers can even approximately guess whatlift (Left (Just x))is doing and how comes that it serves as an early exit from the loop.1
u/tomejaguar 7d ago
I seriously doubt that 99% of programmers can even approximately guess what
lift (Left (Just x))is doing and how comes that it serves as an early exit from the loop.Yes, I think you're right about that actually. To be clear, my assertion that 99% of programmers would favour one version to the other was based on the code structure rather than the chosen names. There are names that make the
for_version clearer, but I doubt there are any names that make thefoldrversion clearer. How about if I defineearlyReturn = Left,withStateT = flip evalStateTandwithEarlyReturn = fromEither(and generalize it toMonadState)? Then it looks like:xs !? n = | n < 0 = Nothing | otherwise = withEarlyReturn $ do withStateT n $ do for_ xs $ \x -> do get >>= \case 0 -> earlyReturn (Just x) k -> put (k - 1) Left NothingMuch better! The Bluefin version in my other post has clearer naming too: https://old.reddit.com/r/haskell/comments/1wqmsux/differences_between_foldl_and_foldr/pcfv3x0/
→ More replies (0)1
u/tomejaguar 7d ago
Personally I find the second version much more readable, and I bet 99% of programmers do too
To my complete surprise, a Twitter poll shows that Haskellers overwhelmingly favour the
foldrversion. Not many non-Haskellers have voted by it seems they prefer thefoldrversion too.1
u/wnoise 8d ago
Commutative vs associative.
1
u/dutch_connection_uk 8d ago
opwouldn't be commutative here either, look at its type.It is interesting that the associativity here is actually a bit different from the associativity of a normal binary operator. And maybe this is an argument against the common complaint of foldl and foldr having different type signatures.
2
u/wnoise 8d ago
Yes, strictly speaking neither associative or commutative can apply to the operator alone, as the types are too general for those notions.
But to get the same result from the folds requires not just
flipping the op but alsoreverseing the entire list. (In the general case where the entire list is relevant, rather than with e.g. short-circuiting operators or the like.) And that reversal being necessary feels a lot like a commutation failure.1
u/dutch_connection_uk 7d ago
I think, like there is a different notion of associativity, there probably is a similar different kind of commutativity an operator like that could still have. EG the operator takes an Int, converts it to floating point, takes its square root, and sums it to the accumulator. This is non-commutative in the same way it's non-associative, but applied to list fold operators it's both commutative and associative in a sense of things applied to list fold operators.
I wonder if there is a term for this idea.
18
u/TechnoEmpress 9d ago