A neat fact about foldr on lists, unlike foldl, is that it actually passes control flow entirely to the accumulating function on each fold step. That means you can use foldr to implement arbitrary traversals of lists, including foldl' or list traversals that exit early. See https://github.com/quchen/articles/blob/master/useful_techni...
If you don’t already know about folds, arguably yes.
If you do, then a quick glance at whether you’re using foldl’ or foldr tells you about what the function is allowed to do, which cuts down a little on comprehension.
List traversals are typically compact enough that there’s not a huge difference either way.
One advantage of using a fold, even in these cases, is that newcomers to Haskell often get so carried away with (and confused by) the power of pattern matching that they’ll write bizarre overly-complicated list traversals by hand, when a simpler mechanism exists that they just haven’t yet internalized.
There's a relatively popular point of view amongst Haskell programmers that explicit recursion is the goto of functional programming; a dedicated folding or traversing function provides more clarity on what exactly the function intends to do. The extreme end of this is recursion schemes and memes like zygohistomorphic prepomorphisms, which are almost certainly overkill on lists proper but might be useful when traversing bigger recursive structures. Personally, I almost always prefer to find a monoid to map the list elements into, and use `fold :: (Monoid m, Foldable t) => t m -> m`. It's essentially the equivalent of using `sum()` instead of `reduce()` in python
In functional programming, there is no "generic list iterator", though. It matters what the output is, whether you need to look at the value of previous iterations, etc.
One of the links (on Fusion) points out that GHC in particular has a lot of optimizations and rewrite rules for folds and many Prelude functions written as simple recursion also include optimized fold representations to suggest to various stages of GHC's optimizer.
As with so many such topics it seems an interesting spectrum between clarity and/or aesthetics and potential performance optimizations. Especially because it often seems like one of those "learning curve flips the clarity/aesthetics preferences" because at some point of familiarity folds can be faster to read than trying to reason through an explicitly written recursion.
Yes, because foldr is equivalent to for_, that is, iterating over a container and performing an effectful action (what in other languages would be called "doing something") for each element. Uses of foldr can always be rewritten to uses of for_, and I find things much clearer in terms of for_!
Well, `foldr` is (up to interdefinability) just the implementation of `for_` (or `traverse`) in the case of lists, in the same way that `map` is the implementation of `fmap`. The point of `traverse` is that it's provided by the functor.
The question of whether to use the most generic (for consistency) or the least generic (for directness) name for a function is the old bugbear of the Prelude :)
I don't quite follow. `fmap = map` is a valid implementation for lists indeed, but `for_ = foldr` is not. They don't have compatible types, for one thing. What do you mean?
Another neat fact is that it is a complete recursion scheme for lists. Since it just ‘replaces the constructors’, it doesn't lose any information from the list: if you can write a function by recursing on a list then you can write it using `foldr`.
In other words, `foldr` (flipped about) takes a list to its Church encoding.
To a CS person, "ordering" is about the arrangement of a list. But pretty much everyone else will think about a *temporal* order of operations. And in that case it is indeed "starting" from the right or left.
It just gets confusing when you're dealing with lots of deferred/lazy operations.
> Both foldl and foldr traverse the structure in the same order, which in the case of lists means left to right.
This characterization is debatable, unless one is assuming single-linked lists, which by definition can only be traversed from left to right (even moreso in a lazy language where the list may have indefinite length). When implemented in a strict language on an array or a double-linked list, the traversal order will be right-to-left for foldr.
Right. The article is only talking about the foldr and foldl functions on Haskell lists. In the addendum it talks about the functions on other data structures (presumably the implementation of instances of the Foldable class). It would be more instructive to examine the definitions of foldr and foldl on other types such as Map instead of yet another custom list type.
I actually think examining the definitions of folds in Map (a binary tree) is perhaps pedagogically a better starting point. The singly linked list is inherently left biased. A binary tree is symmetrical. So the implementation of foldr and foldl on a binary tree is more similar: literally flipping the order of the arguments to the accumulation function and swapping the left and right children. Furthermore you can induce the “early termination” by laziness behavior by adjusting whether your accumulation function forces the first or second argument. And all four versions foldr, foldl, foldr' and foldl' are meaningful.
That's not how I think about it: to me the fundamental difference between foldl and foldr isn't how the list is traversed but how the monoid is applied: left-to-right for foldl and right-to-left for foldr. Is this an inaccurate characterization?
I'm not sure what the point you're trying to illustrate is, but I think you forgot a `flip` (unless the point is that you always mix up the argument order of the foldr reducing function, in which case fair enough).
35 comments
[ 0.29 ms ] story [ 7.4 ms ] threadIf you do, then a quick glance at whether you’re using foldl’ or foldr tells you about what the function is allowed to do, which cuts down a little on comprehension.
List traversals are typically compact enough that there’s not a huge difference either way.
One advantage of using a fold, even in these cases, is that newcomers to Haskell often get so carried away with (and confused by) the power of pattern matching that they’ll write bizarre overly-complicated list traversals by hand, when a simpler mechanism exists that they just haven’t yet internalized.
Most recursion is through fold and unfold
As with so many such topics it seems an interesting spectrum between clarity and/or aesthetics and potential performance optimizations. Especially because it often seems like one of those "learning curve flips the clarity/aesthetics preferences" because at some point of familiarity folds can be faster to read than trying to reason through an explicitly written recursion.
(This is explained in my article "foldl traverses with State, foldr traverses with anything": https://h2.jaguarpaw.co.uk/posts/foldl-traverses-state-foldr...)
The question of whether to use the most generic (for consistency) or the least generic (for directness) name for a function is the old bugbear of the Prelude :)
In other words, `foldr` (flipped about) takes a list to its Church encoding.
The compiler itself acts as a steering function for the AI. I've also had this experience with Rust.
It just gets confusing when you're dealing with lots of deferred/lazy operations.
This characterization is debatable, unless one is assuming single-linked lists, which by definition can only be traversed from left to right (even moreso in a lazy language where the list may have indefinite length). When implemented in a strict language on an array or a double-linked list, the traversal order will be right-to-left for foldr.
I actually think examining the definitions of folds in Map (a binary tree) is perhaps pedagogically a better starting point. The singly linked list is inherently left biased. A binary tree is symmetrical. So the implementation of foldr and foldl on a binary tree is more similar: literally flipping the order of the arguments to the accumulation function and swapping the left and right children. Furthermore you can induce the “early termination” by laziness behavior by adjusting whether your accumulation function forces the first or second argument. And all four versions foldr, foldl, foldr' and foldl' are meaningful.
That's not how I think about it: to me the fundamental difference between foldl and foldr isn't how the list is traversed but how the monoid is applied: left-to-right for foldl and right-to-left for foldr. Is this an inaccurate characterization?