What constitutes a fold for types other than list?
Solution 1:
A Fold for Every Occasion
We can actually come up with a generic notion of folding which can apply to a whole bunch of different types. That is, we can systematically define a fold
function for lists, trees and more.
This generic notion of fold
corresponds to the catamorphisms @pelotom mentioned in his comment.
Recursive Types
The key insight is that these fold
functions are defined over recursive types. In particular:
data List a = Cons a (List a) | Nil
data Tree a = Branch (Tree a) (Tree a) | Leaf a
Both of these types are clearly recursive--List
in the Cons
case and Tree
in the Branch
case.
Fixed Points
Just like functions, we can rewrite these types using fixed points. Remember the definition of fix
:
fix f = f (fix f)
We can actually write something very similar for types, except that it has to have an extra constructor wrapper:
newtype Fix f = Roll (f (Fix f))
Just like fix
defines the fixed point of a function, this defines the fixed point of a functor. We can express all our recursive types using this new Fix
type.
This allows us to rewrite List
types as follows:
data ListContainer a rest = Cons a rest | Nil
type List a = Fix (ListContainer a)
Essentially, Fix
allows us to nest ListContainer
s to arbitrary depths. So we could have:
Roll Nil
Roll (Cons 1 (Roll Nil))
Roll (Cons 1 (Roll (Cons 2 (Roll Nil))))
which correspond to []
, [1]
and [1,2]
respectively.
Seeing that ListContainer
is a Functor
is easy:
instance Functor (ListContainer a) where
fmap f (Cons a rest) = Cons a (f rest)
fmap f Nil = Nil
I think the mapping from ListContainer
to List
is pretty natural: instead of recursing explicitly, we make the recursive part a variable. Then we just use Fix
to fill that variable in as appropriate.
We can write an analogous type for Tree
as well.
"Unwrapping" Fixed Points
So why do we care? We can define fold
for arbitrary types written using Fix
. In particular:
fold :: Functor f => (f a -> a) -> (Fix f -> a)
fold h = h . fmap (fold h) . unRoll
where unRoll (Roll a) = a
Essentially, all a fold does is unwrap the "rolled" type one layer at a time, applying a function to the result each time. This "unrolling" lets us define a fold for any recursive type and neatly and naturally generalize the concept.
For the list example, it works like this:
- At each step, we unwrap the
Roll
to get either aCons
or aNil
- We recurse over the rest of the list using
fmap
.- In the
Nil
case,fmap (fold h) Nil = Nil
, so we just returnNil
. - In the
Cons
case, thefmap
just continues the fold over the rest of the list.
- In the
- In the end, we get a bunch of nested calls to
fold
ending in aNil
--just like the standardfoldr
.
Comparing Types
Now lets look at the types of the two fold functions. First, foldr
:
foldr :: (a -> b -> b) -> b -> [a] -> b
Now, fold
specialized to ListContainer
:
fold :: (ListContainer a b -> b) -> (Fix (ListContainer a) -> b)
At first, these look completely different. However, with a bit of massaging, we can show they're the same. The first two arguments to foldr
are a -> b -> b
and b
. We have a function and a constant. We can think of b
as () -> b
. Now we have two functions _ -> b
where _
is ()
and a -> b
. To make life simpler, let's curry the second function giving us (a, b) -> b
. Now we can write them as a single function using Either
:
Either (a, b) () -> b
This is true because given f :: a -> c
and g :: b -> c
, we can always write the following:
h :: Either a b -> c
h (Left a) = f a
h (Right b) = g b
So now we can view foldr
as:
foldr :: (Either (a, b) () -> b) -> ([a] -> b)
(We are always free to add parentheses around ->
like this as long as they're right-associative.)
Now lets look at ListContainer
. This type has two cases: Nil
, which carries no information and Cons
, which has both an a
and a b
. Put another way, Nil
is like ()
and Cons
is like (a, b)
, so we can write:
type ListContainer a rest = Either (a, rest) ()
Clearly this is the same as what I used in foldr
above. So now we have:
foldr :: (Either (a, b) () -> b) -> ([a] -> b)
fold :: (Either (a, b) () -> b) -> (List a -> b)
So, in fact, the types are isomorphic--just different ways of writing the same thing! I think that's pretty cool.
(As a side note, if you want to know more about this sort of reasoning with types, check out The Algebra of Algebraic Data Types, a nice series of blog posts about just this.)
Back to Trees
So, we've seen how we can define a generic fold
for types written as fixed points. We've also seen how this corresponds directly to foldr
for lists. Now lets look at your second example, the binary tree. We have the type:
data Tree a = Branch a (Tree a) (Tree a) | Leaf a
we can rewrite this using Fix
by following the rules I did above: we replace the recursive part with a type variable:
data TreeContainer a rest = Branch rest rest | Leaf a
type Tree a = Fix (TreeContainer a)
Now we have a tree fold
:
fold :: (TreeContainer a b -> b) -> (Tree a -> b)
Your original foldTree
looks like this:
foldTree :: (b -> b -> b) -> (a -> b) -> Tree a -> b
foldTree
accepts two functions; we'll combine the into one by first currying and then using Either
:
foldTree :: (Either (b, b) a -> b) -> (Tree a -> b)
We can also see how Either (b, b) a
is isomorphic to TreeContainer a b
. Tree container has two cases: Branch
, containing two b
s and Leaf
, containing one a
.
So these fold types are isomorphic in the same way as the list example.
Generalizing
There is a clear pattern emerging. Given a normal recursive data type, we can systematically create a non-recursive version of the type, which lets us express the type as a fixed point of a functor. This means that we can mechanically come up with fold
functions for all these different types--in fact, we could probably automate the entire process using GHC Generics or something like that.
In a sense, this means that we do not really have different fold
functions for different types. Rather, we have a single fold
function which is very polymorphic.
More
I first fully understood these ideas from a talk given by Conal Elliott. This goes into more detail and also talks about unfold
, which is the dual to fold
.
If you want to delve into this sort of thing even more deeply, read the fantastic "Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire" paper. Among other things, this introduces the notions of "catamorphisms" and "anamorphisms" which correspond to folds and unfolds.
Algebras (and Coalgebras)
Also, I can't resist adding a plug for myself :P. You can see some interesting similarities between the way we use Either
here and the way I used it when talking about algebras in another SO answer.
There is in fact a deep connection between fold
and algebras; moreover, unfold
--the aforementioned dual of fold
--is connected to coalgebras, which are the dual of algebras. The important idea is that algebraic data types correspond to "initial algebras" which also define folds as outlined in the rest of my answer.
You can see this connection in the general type of fold
:
fold :: Functor f => (f a -> a) -> (Fix f -> a)
The f a -> a
term looks very familiar! Remember that an f-algebra was defined as something like:
class Functor f => Algebra f a where
op :: f a -> a
So we can think of fold
as just:
fold :: Algebra f a => Fix f -> a
Essentially, fold
just lets us "summarize" structures defined using the algebra.
Solution 2:
Tikhon's got the technical stuff down. I think I'll try to simplify down from what he said.
The term "folding" has, unfortunately, become ambiguous over the years to mean one of two things:
- Reducing a collection sequentially in some order. In Haskell, this is what "folding" means in the
Foldable
class, which larsmans brings up. - The notion you asked for: "destructing" (opposite of constructing), "observing" or "eliminating" an algebraic data type according to its structure. Also called a catamorphism.
It is possible to define both of these notions generically so that one parametrized function is capable of doing it for a variety of types. Tikhon shows how to do in the second case.
But most often going the whole way with Fix
and the algebras and such is overkill. Let's work out a simpler way of writing the fold for any algebraic data type. We'll use Maybe
, pairs, lists and trees as our examples:
data Maybe a = Nothing | Just a
data Pair a b = Pair a b
data List a = Nil | Cons a (List a)
data Tree x = Leaf x | Branch (Tree x) (Tree x)
data BTree a = Empty | Node a (BTree a) (BTree a)
Note that Pair
is not recursive; the procedure I'm going to show doesn't assume that the "fold" type is recursive. People don't usually call this case a "fold," but it's really the non-recursive case of the same concept.
First step: the fold for a given type will consume the folded type and produce some parameter type as its result. I like to call the latter r
(for "result"). So:
foldMaybe :: ... -> Maybe a -> r
foldPair :: ... -> Pair a b -> r
foldList :: ... -> List a -> r
foldTree :: ... -> Tree a -> r
foldBTree :: ... -> BTree a -> r
Second step: in addition to the last argument (the one for the structure), the fold takes as many arguments as the type has constructors. Pair
has one constructor and our other examples have two, so:
foldMaybe :: nothing -> just -> Maybe a -> r
foldPair :: pair -> Pair a b -> r
foldList :: nil -> cons -> List a -> r
foldTree :: leaf -> branch -> Tree a -> r
foldBTree :: empty -> node -> BTree a -> r
Third step: each of these arguments has the same arity as the constructor it corresponds to. Let's treat the constructors as functions, and write out their types (making sure the type variables match up with the ones in the signatures we're writing):
Nothing :: Maybe a
Just :: a -> Maybe a
Pair :: a -> b -> Pair a b
Nil :: List a
Cons :: a -> List a -> List a
Leaf :: a -> Tree a
Branch :: Tree a -> Tree a -> Tree a
Empty :: BTree a
Node :: a -> BTree a -> BTree a -> BTree a
Step 4: in the signature of each constructor, we will replace all occurrences of the data type it constructs with our type variable r
(that we're using in our fold signatures):
nothing := r
just := a -> r
pair := a -> b -> r
nil := r
cons := a -> r -> r
leaf := a -> r
branch := r -> r -> r
empty := r
node := a -> r -> r -> r
As you can see, I've "assigned" the resulting signatures to my dummy type variables from the second step. Now Step 5: fill those in into the earlier sketch fold signatures:
foldMaybe :: r -> (a -> r) -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: r -> (a -> r -> r) -> List a -> r
foldTree :: (a -> r) -> (r -> r -> r) -> Tree a -> r
foldBTree :: r -> (a -> r -> r -> r) -> BTree a -> r
Now, these are signatures for the folds of those types. They have a funny argument order, because I did this mechanically by reading the fold type off the data
declarations and constructor types, but for some reason in functional programming it's conventional to put base cases first in data
definitions yet recursive case handlers first in fold
definitions. No problem! Let's reshuffle them to make them more conventional:
foldMaybe :: (a -> r) -> r -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: (a -> r -> r) -> r -> List a -> r
foldTree :: (r -> r -> r) -> (a -> r) -> Tree a -> r
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
The definitions can also be filled in mechanically. Let's pick foldBTree
and implement it step by step. The fold for a given type is the one function of the type we figured out that meets this condition: folding with the type's constructors is an identity function over that type (you get the same result as the value you started with).
We'll start like this:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree = ???
We know it takes three arguments, so we can add variables to reflect them. I'll use long descriptive names:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty tree = ???
Looking at the data
declaration, we know BTree
has two possible constructors. We can split the definition into a case for each, and fill out variables for their elements:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = ???
foldBTree branch empty (Branch a l r) = ???
-- Let's use comments to keep track of the types:
-- a :: a
-- l, r :: BTree a
Now, short of something like undefined
, the only way to fill in the first equation is with empty
:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = ???
-- a :: a
-- l, r :: BTree a
How do we fill in the second equation? Again, short of undefined
, we have this:
branch :: a -> r -> r -> r
a :: a
l, r :: BTree a
If we had subfold :: BTree a -> r
, we could do branch a (subfold l) (subfold r) :: r
. But of course, we can write 'subfold' easily:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = branch a (subfold l) (subfold r)
where subfold = foldBTree branch empty
This is the fold for BTree
, because foldBTree Branch Empty anyTree == anyTree
. Note that foldBTree
isn't the only function of this type; there's also this:
mangleBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
mangleBTree branch empty Empty = empty
mangleBTree branch empty (Branch a l r) = branch a (submangle r) (submangle l)
where submangle = mangleBTree branch empty
But in general, mangleBTree
doesn't have the required property; for example if we have foo = Branch 1 (Branch 2 Empty Empty) Empty
, it follows that mangleBTree Branch Empty foo /= foo
. So mangleBTree
, though it has the correct type, is not the fold.
Now, let's take a step back from details, and concentrate on that last point with the mangleTree
example. A fold (in the structural sense, #2 at the top of my answer) is nothing more and nothing else than the simplest, non-trivial function for an algebraic type such that, when you give pass in the type's constructors as its arguments, it becomes the identity function for that type. (By nontrivial I mean that things like foo f z xs = xs
is not allowed.)
This is very significant. Two ways I like to think about it are as follows:
- The fold for a given type can "see" all the information contained by any value of that type. (This is why it's able to perfectly "reconstruct" any value of that type from the ground up using the type's constructors.)
- The fold is the most general "consumer" function for that type. Any function that consumes a value of the type in question can be written so that the only operations it uses from that type are the fold and the constructors. (Though the fold-only versions of some functions are hard to write and perform badly; try writing
tail :: [a] -> [a]
withfoldr
,(:)
and[]
as a painful exercise.)
And the second point goes even further, in that you don't even need the constructors. You can implement any algebraic type without using data
declarations or constructors, using nothing but folds:
{-# LANGUAGE RankNTypes #-}
-- | A Church-encoded list is a function that takes the two 'foldr' arguments
-- and produces a result from them.
newtype ChurchList a =
ChurchList { runList :: forall r.
(a -> r -> r) -- ^ first arg of 'foldr'
-> r -- ^ second arg of 'foldr'
-> r -- ^ 'foldr' result
}
-- | Convenience function: make a ChurchList out of a regular list
toChurchList :: [a] -> ChurchList a
toChurchList xs = ChurchList (\kons knil -> foldr kons knil xs)
-- | 'toChurchList' isn't actually needed, however, we can make do without '[]'
-- completely.
cons :: a -> ChurchList a -> ChurchList a
cons x xs = ChurchList (\f z -> f x (runlist xs f z))
nil :: ChurchList a
nil = ChurchList (\f z -> z)
foldr' :: (a -> r -> r) -> r -> ChurchList a -> r
foldr' f z xs = runList xs f z
head :: ChurchList a -> Maybe a
head = foldr' ((Just .) . const) Nothing
append :: ChurchList a -> ChurchList a -> ChurchList a
append xs ys = foldr' cons ys xs
-- | Convert a 'ChurchList' to a regular list.
fromChurchList :: ChurchList a -> [a]
fromChurchList xs = runList xs (:) []
As an exercise you can try writing other types in this way (which uses the RankNTypes
extension—read this for a primer). This technique is called Church encoding, and is sometimes useful in actual programming—for example, GHC uses something called foldr
/build
fusion to optimize list code to remove intermediate lists; see this Haskell Wiki page, and note the type of build
:
build :: (forall b. (a -> b -> b) -> b -> b) -> [a]
build g = g (:) []
Except for the newtype
, this is the same as my fromChurchList
above. Basically, one of the rules that GHC uses to optimize list processing code is this:
-- Don't materialize the list if all we're going to do with it is
-- fold it right away:
foldr kons knil (fromChurchList xs) ==> runChurchList xs kons knil
By implementing the basic list functions to use Church encodings internally, inlining their definitions aggressively, and applying this rule to the inlined code, nested uses of functions like map
can be fused into a tight loop.
Solution 3:
A fold replaces every constructor with a function.
For example, foldr cons nil
replaces every (:)
with cons
and []
with nil
:
foldr cons nil ((:) 1 ((:) 2 [])) = cons 1 (cons 2 nil)
For a tree, foldTree branch leaf
replaces every Branch
with branch
and every Leaf
with leaf
:
foldTree branch leaf (Branch (Branch (Leaf 1) (Leaf 2)) (Leaf 3))
= branch (branch (leaf 1) (leaf 2)) (leaf 2)
This is why every fold accepts arguments that have the exact same type as the constructors:
foldr :: (a -> list -> list) -> list -> [a] -> list
foldTree :: (tree -> tree -> tree) -> (a -> tree) -> Tree a -> tree