5. Recursion schemes¶
Traversing an EADT explicitly (see Explicit recursive traversal) can be tedious. Another approach consists in using dedicated composable combinators called recursion schemes.
The well known map and fold functions are examples of recursion schemes
for lists: these functions handle the recursive traversal of the data structure
and are parameterized by the functions performing the actual work. Recursion
schemes are a generalization of this approach.
The best introduction to recursion schemes I’ve read can be found here: https://blog.sumtypeofway.com/an-introduction-to-recursion-schemes/
To avoid paraphrasing, I recommend that you read it before continuing.
See also: https://bartoszmilewski.com/2013/06/10/understanding-f-algebras/
5.1. Catamorphism: Show example¶
Suppose we rewrite our Show class like this:
class FunctorShow (f :: * -> *) where
functorShow :: f String -> String
We can define instances for NilF and ConsF:
instance FunctorShow NilF where
functorShow _ = "Nil"
instance (Show a) => FunctorShow (ConsF a) where
functorShow (ConsF a l) = show a ++ " : " ++ l
Note that there is no recursive call in the definition of ConsF’s instance:
it is because we are going to use a recursion scheme that will handle the
recursion.
We also need an instance to handle the generic VariantF type:
instance (AlgVariantF FunctorShow String xs) => FunctorShow (VariantF xs) where
functorShow = algVariantF @FunctorShow functorShow
Finally we can define a generic eadtShow function that uses the catamorphism
recursion scheme with the functorShow class method.
eadtShow ::
( Functor (VariantF xs)
, FunctorShow (VariantF xs)
) => EADT xs -> String
eadtShow = cata functorShow
We can test it:
intList :: List Int
intList = Cons (10 :: Int) $ Cons (20 :: Int) $ Cons (30 :: Int) Nil
mixedList :: EADT '[ConsF Int, ConsF Float, ConsF String, NilF]
mixedList = Cons @Int 10 $ Cons @Float 5.0 $ Cons "Test" Nil
> putStrLn $ eadtShow intList
10 : 20 : 30 : Nil
> putStrLn $ eadtShow mixedList
10 : 5.0 : "Test" : Nil
5.2. Catamorphism: List (a -> a) mapping example¶
Similarily to the example above, suppose that we want to implement mapping over an EADT list. We can use the following type-class:
class MapEADT a xs (f :: * -> *) where
-- map the outer constructor of an EADT
mapEADT1 :: (a -> a) -> f (EADT xs) -> EADT xs
We need some instances to handle our EADT constructors:
instance (NilF :<: xs) => MapEADT a xs NilF where
mapEADT1 _ NilF = Nil
instance (ConsF a :<: xs) => MapEADT a xs (ConsF a) where
mapEADT1 f (ConsF a x) = Cons (f a) x
And a additional instance to traverse the VariantF combinator datatype:
instance (AlgEADT (MapEADT a xs) xs) => MapEADT a xs (VariantF xs) where
mapEADT1 f = algVariantF @(MapEADT a xs) (mapEADT1 f)
Now we can define the mapEADT function by using the catamorphism combinator:
-- recursively map an EADT
mapEADT :: ( Functor (VariantF xs)
, MapEADT a xs (VariantF xs)
) => (a -> a) -> EADT xs -> EADT xs
mapEADT f = cata (mapEADT1 f)
We can test it:
intList :: List Int
intList = Cons (10 :: Int) $ Cons (20 :: Int) $ Cons (30 :: Int) Nil
> putStrLn $ eadtShow $ mapEADT ((+5) :: Int -> Int) intList
15 : 25 : 35 : Nil
5.3. Catamorphism: List (a -> b) mapping example¶
Similarily, we can also support mapping with a function that changes the EADT type as follow:
class TransEADT a b xs xs' (f :: * -> *) where
transEADT1 :: (a -> b) -> f (EADT xs) -> EADT xs'
instance (NilF :<: xs') => TransEADT a b xs xs' NilF where
transEADT1 _ NilF = Nil
instance (ConsF b :<: xs', xs ~ xs') => TransEADT a b xs xs' (ConsF a) where
transEADT1 f (ConsF a x) = Cons (f a) x
instance TransEADT a b xs xs' (VariantF []) where
transEADT1 _ _ = undefined
instance
( TransEADT a b xs xs' f
, TransEADT a b xs xs' (VariantF fs)
) => TransEADT a b xs xs' (VariantF (f : fs)) where
transEADT1 f v = case popVariantFHead v of
Right u -> transEADT1 f u
Left w -> transEADT1 f w
transEADT :: ( Functor (VariantF xs)
, TransEADT a b xs' xs' (VariantF xs)
) => (a -> b) -> EADT xs -> EADT xs'
transEADT f = cata (transEADT1 f)
Note that we need to specify the resulting type as it can be anything fulfilling the constraints:
> putStrLn $ eadtShow $ (transEADT (fromIntegral :: Int -> Float) intList :: List Float)
10.0 : 20.0 : 30.0 : Nil