2. Explicit recursive traversal

When we need to traverse a data structure, we can either use predefined traversal functions (e.g., map, fold, etc.) or write the recursive function explicitly. EADTs are no different in this regard.

In this chapter we explain how to write explicitly recursive functions for EADTs: similarly to usual ADTs, it’s better to use them only when generic traversal functions (presented in following chapters) don’t fit the bill.

2.1. Traversal example

If we were to write a show function for a list ADT, we could do it like this:

data List a = Cons a (List a) | Nil

showList :: Show a => List a -> String
showList = \case
   Nil      -> "Nil"
   Cons a l -> show a ++ " : " ++ showList l

In showList we can pattern match on the constructors of List a because the constructor list is closed. With EADTs the list of constructors isn’t closed and we want to be able to use the same code even with EADTs extended with more constructors. To support this, we use type-classes to build the equivalent of the case in showList above.

Let’s define a class MyShow that is very much like Show and that we will use to print any EADT value:

class MyShow e where
   myShow :: e -> String

We can define instances for the List constructors defined in a previous chapter:

instance MyShow (NilF e) where
   myShow _ = "Nil"

instance (MyShow e, Show a) => MyShow (ConsF a e) where
   myShow (ConsF a l) = show a ++ " : " ++ myShow l

Note how each instance corresponds to an alternative in showList.

It also requires some additional instances to traverse the VariantF combinator datatype and the Fix recursivity handling datatype:

{-# LANGUAGE UndecidableInstances #-}

instance MyShow (f (Fix f)) => MyShow (Fix f) where
   {-# INLINE myShow #-}
   myShow (Fix e) = myShow e

instance MyShow (VariantF '[] e) where
   {-# INLINE myShow #-}
   myShow = undefined

instance
      ( MyShow (f e)
      , MyShow (VariantF fs e)
      ) => MyShow (VariantF (f ': fs) e)
   where
      {-# INLINE myShow #-}
      myShow v = case popVariantFHead v of
         Right u -> myShow u
         Left  w -> myShow w

Note

This boilerplate code (hopefully always very similar and straightforward) is the main reason you should strive to use predefined recursion schemes instead of the explicit approach presented here.

Note

The INLINE pragmas are used to ensure that in the generated code we get the equivalent of the case expression in showList.

Now we can test it:

strList :: List String
strList = Cons "How" (Cons "are" (Cons "you?" Nil))

intList :: List Int
intList = Cons (10 :: Int) $ Cons (20 :: Int) $ Cons (30 :: Int) Nil

mixedList :: EADT '[ConsF Int, ConsF Float, NilF]
mixedList = Cons (10 :: Int) $ Cons (5.0 :: Float) $ Cons (30 :: Int) Nil

> putStrLn (myShow strList)
"How" : "are" : "you?" : Nil

> putStrLn (myShow intList)
10 : 20 : 30 : Nil

> putStrLn (myShow mixedList)
10 : 5.0 : 30 : Nil

2.2. Extension example

If we add a new constructor, such as NodeF to build binary trees:

data NodeF a l = NodeF a l l deriving (Functor)

eadtPattern 'NodeF "Node"

We can also add a MyShow instance for NodeF:

instance (MyShow e, Show a) => MyShow (NodeF a e) where
   myShow (NodeF a l1 l2) = show a ++ "\n|- " ++ indent (myShow l1)
                                   ++ "|- " ++ indent (myShow l2)
      where
         indent' []     = []
         indent' (x:xs) = x : fmap ("   "++) xs
         indent = unlines . indent' . lines

Now we can show trees as well as lists:

tree :: EADT '[NodeF Int, NilF]
tree = Node (10 :: Int)
         (Node (5 :: Int) Nil Nil)
         (Node (30 :: Int) Nil Nil)


> putStr (myShow tree)
10
|- 5
   |- Nil
   |- Nil
|- 30
   |- Nil
   |- Nil

We can also mix up trees and lists by using ConsF and NodeF in the same EADT:

mixedTree :: EADT '[NodeF Int, ConsF Int, NilF]
mixedTree = Node (10 :: Int)
         (Cons (5 :: Int) $ Cons (6 :: Int) $ Cons (7 :: Int) Nil)
         (Node (30 :: Int) Nil Nil)

> putStr (myShow mixedTree)
10
|- 5 : 6 : 7 : Nil
|- 30
   |- Nil
   |- Nil