4

Say I have some simplified lisp-style Expr type as follows

data Expr = String String | Cons Expr Expr
  deriving (Show)

I can create lists as Cons-cells of Cons-cells:

Cons (String "hello") (Cons (String "World") (String "!"))

From this I would like to implement Foldable for Expr to fold over these cons lists - but that's not possible, since Foldable requires a type of kind * -> * (i.e. polymorphic with exactly one type parameter), wheres my Expr has kind *.

Why is that? To me it seems like folding over non-polymorphic types like in this case would be perfectly reasonable, but obviously I'm missing something.

duplode
  • 33,731
  • 7
  • 79
  • 150
henrikl
  • 479
  • 2
  • 14
  • 2
    It's because a `Foldable` instance is a kind of "container", and doesn't care what type is inside it. Your own type could be made polymorphic by replacing the `String` with any arbitrary type - and that wouldn't change anything about the structure. – Robin Zigmond Apr 16 '19 at 20:11
  • 3
    See also https://stackoverflow.com/questions/39634504/is-there-anything-we-lose-with-monofoldable – Joseph Sible-Reinstate Monica Apr 16 '19 at 20:23

1 Answers1

5

To me it seems like folding over non-polymorphic types like in this case would be perfectly reasonable, but obviously I'm missing something.

It is perfectly reasonable indeed. One way of folding a monomorphic container is using MonoFoldable. Another is using a Fold from lens, or from some other optics library:

import Control.Lens

data Expr = String String | Cons Expr Expr
  deriving (Show)

-- A Traversal can also be used as a Fold.
-- strings :: Applicative f => (String -> f String) -> (Expr -> f Expr) 
strings :: Traversal' Expr String
strings f (String s) = String <$> f s
strings f (Cons l r) = Cons <$> strings f l <*> strings f r 
GHCi> hello = Cons (String "hello") (Cons (String "World") (String "!"))
GHCi> toListOf strings hello
["hello","World","!"]
GHCi> import Data.Monoid
GHCi> foldMapOf strings (Sum . length) hello
Sum {getSum = 11}

As for why Foldable instances have kind * -> * rather than *, I would put it down to a combination of simplicity and historical reasons. Historically speaking, Foldable is an offshoot of Traversable, and it is worth noting that, while monomorphic traversals can be useful, their limitations are rather more striking than those which affect monomorphic folds (for instance, you can't recover fmap from them, but merely a monomorphic omap). Finally, the Q&A suggested by Joseph Sible, Is there anything we lose with MonoFoldable?, includes some interesting discussion of potential reasons for not outright replacing Foldable with MonoFoldable.

duplode
  • 33,731
  • 7
  • 79
  • 150
  • Thanks! I feel like most of the details still fly over my head, but you've given me some great pointers to explore! – henrikl Apr 17 '19 at 08:40
  • @ionree In case it helps, a few more words on optics: one of the things they allow us is, so to say, supply substitutes for functor/foldable/traversable/etc. instances on the fly (and, as seen here, those substitutes don't necessarily have to be polymorphic). – duplode Apr 17 '19 at 21:47