6
votes

I'm experimenting with the Foldable typeclass in Haskell, using the following data type as an example:

data Tree a = Empty
            | Node (Tree a) a (Tree a)

If I use the DeriveFoldable GHC extension, it seems to derive a Foldable instance along the lines of

instance Foldable Tree where
  foldMap _ Empty = mempty
  foldMap f (Node l n r) = (foldMap f l) <> (f n) <> (foldMap f r)

i.e., an inorder traversal of the tree. However, I don't see anything obvious preventing a different Foldable instance, such as a preorder traversal:

instance Foldable Tree where
  foldMap _ Empty = mempty
  foldMap f (Node l n r) = (f n) <> (foldMap f l) <> (foldMap f r)

Are there laws for the Foldable typeclass that would make the preorder traversal instance unlawful?

3
The reason it derives it that way is because it uses the order of the parameters, if you defined it as Node a (Tree a) (Tree a), it would be preorder traversal.Willem Van Onsem
For a functor instance, it should satisfy foldMap f = fold . fmap f (as is specified in the documentation).Willem Van Onsem
I do not see a problem with defining it with pre-order traversal. Although I would find it more sensical to do this with Node a (Tree a) (Tree a), since then the datastructure "hints" how the foldable will look like.Willem Van Onsem
@WillemVanOnsem So in short, there aren't any laws constraining how to create the instance, and DeriveFoldable just uses the parameter order? Sounds like a good answer.DylanSp
well it should satisfy some laws, like sum = getSum . foldMap Sum, etc. to sum up the type. In essence the idea is that you fold over it satisfying monadic laws.Willem Van Onsem

3 Answers

6
votes

Foldable has no laws guiding the order of traversal. In fact, we can think of the act of writing a Foldable instance as choosing a specific order of traversal. If DeriveFoldable is used, the choice will be to follow the order of the fields in the definition of the type (see also Daniel Wagner's answer); the details are documented in the relevant section of the GHC User's Guide.

(Side note: As discussed in dfeuer's answer, Traversable has richer laws that, among other things, limit the range of acceptable foldMap implementations. Still, both inorder and preorder traversals for your Tree give out lawful implementations of Traversable.)

6
votes

Foldable itself is very poor in laws. The fundamental method is foldMap. Other methods are expected to behave like their default definitions up to laziness details. Two laws arise from parametricity:

  1. If g :: m -> n is a monoid morphism and f :: a -> m is a function, then

    g . foldMap f = foldMap (g . f)
    
  2. If the Foldable is also a Functor, then for all functions g :: b -> m and f :: a -> b,

    foldMap (g . f) = foldMap g . fmap f
    

In practice, most Foldable instances are also Traversable. Traversable has much richer laws for traverse, and imposes the law that

foldMap f = getConst . traverse (Const . f)

This guarantees that for a Traversable, every element in the container is folded over exactly once. Whereas

instance Foldable [] where
  foldMap _ [] = mempty
  foldMap f (x:_:xs) = f x <> f x <> foldMap f xs

would be perfectly valid, there's no lawful Traversable instance that matches.

5
votes

No, there are no laws that tell what order fields must be visited in. It is conventional to visit fields in the order they appear in the data structure definition (or at least the user-visible API, if pattern synonyms are used), but this is convention only.