Histomorphisms, Zygomorphisms and Futumorphisms specialised to lists
Asked Answered
S

3

43

I ended up figuring it out. See the video and slides of a talk I gave:

Original question:

In my effort to understand generic recursion schemes (i.e., that use Fix) I have found it useful to write list-only versions of the various schemes. It makes it much easier to understand the actual schemes (without the additional overhead of the Fix stuff).

However, I have not yet figured out how to define list-only versions of zygo and futu.

Here are my specialised definitions so far:

cataL :: (a ->        b -> b) -> b -> [a] -> b
cataL f b (a : as) = f a    (cataL f b as)
cataL _ b []       = b

paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f b (a : as) = f a as (paraL f b as)
paraL _ b []       = b

-- TODO: histo

-- DONE: zygo (see below)

anaL  :: (b ->       (a, b))               -> b -> [a]
anaL  f b = let (a, b') = f b in a : anaL f b'

anaL' :: (b -> Maybe (a, b))               -> b -> [a]
anaL' f b = case f b of
    Just (a, b') -> a : anaL' f b'
    Nothing      -> []

apoL :: ([b] -> Maybe (a, Either [b] [a])) -> [b] -> [a]
apoL f b = case f b of
    Nothing -> []
    Just (x, Left c)  -> x : apoL f c
    Just (x, Right e) -> x : e

-- DONE: futu (see below)

hyloL  :: (a -> c -> c) -> c -> (b -> Maybe (a, b)) -> b -> c
hyloL f z g = cataL f z . anaL' g

hyloL' :: (a -> c -> c) -> c -> (c -> Maybe (a, c))      -> c
hyloL' f z g = case g z of
    Nothing     -> z
    Just (x,z') -> f x (hyloL' f z' g)

How do you define histo, zygo and futu for lists?

Shere answered 25/4, 2016 at 21:53 Comment(4)
do you know the type signature zygo and futu should have?Nydia
zygo Fix version: zygo :: Functor f => (f b -> b) -> (f (a, b) -> a) -> Fix f -> aShere
futu Mu version: futu :: (Mu b, Functor (PF b)) => Ann b -> (a -> F b (Futu b a)) -> a -> b (see see hackage.haskell.org/package/pointless-haskell-0.0.9/docs/…)Shere
I do not know the list-only type signature - still trying to figure it out.Shere
U
42

Zygomorphism is the high-falutin' mathsy name we give to folds built from two semi-mutually recursive functions. I'll give an example.

Imagine a function pm :: [Int] -> Int (for plus-minus) which intersperses + and - alternately through a list of numbers, such that pm [v,w,x,y,z] = v - (w + (x - (y + z))). You can write it out using primitive recursion:

lengthEven :: [a] -> Bool
lengthEven = even . length

pm0 [] = 0
pm0 (x:xs) = if lengthEven xs
             then x - pm0 xs
             else x + pm0 xs

Clearly pm0 is not compositional - you need to inspect the length of the whole list at each position to determine whether you're adding or subtracting. Paramorphism models primitive recursion of this sort, when the folding function needs to traverse the whole subtree at each iteration of the fold. So we can at least rewrite the code to conform to an established pattern.

paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f z [] = z
paraL f z (x:xs) = f x xs (paraL f z xs)

pm1 = paraL (\x xs acc -> if lengthEven xs then x - acc else x + acc) 0

But this is inefficient. lengthEven traverses the whole list at each iteration of the paramorphism resulting in an O(n2) algorithm.


We can make progress by noting that both lengthEven and para can be expressed as a catamorphism with foldr...

cataL = foldr

lengthEven' = cataL (\_ p -> not p) True
paraL' f z = snd . cataL (\x (xs, acc) -> (x:xs, f x xs acc)) ([], z)

... which suggests that we may be able to fuse the two operations into a single pass over the list.

pm2 = snd . cataL (\x (isEven, total) -> (not isEven, if isEven
                                                      then x - total
                                                      else x + total)) (True, 0)

We had a fold which depended on the result of another fold, and we were able to fuse them into one traversal of the list. Zygomorphism captures exactly this pattern.

zygoL :: (a -> b -> b) ->  -- a folding function
         (a -> b -> c -> c) ->  -- a folding function which depends on the result of the other fold
         b -> c ->  -- zeroes for the two folds
         [a] -> c
zygoL f g z e = snd . cataL (\x (p, q) -> (f x p, g x p q)) (z, e)

On each iteration of the fold, f sees its answer from the last iteration as in a catamorphism, but g gets to see both functions' answers. g entangles itself with f.

We'll write pm as a zygomorphism by using the first folding function to count whether the list is even or odd in length and the second one to calculate the total.

pm3 = zygoL (\_ p -> not p) (\x isEven total -> if isEven
                                                then x - total
                                                else x + total) True 0

This is classic functional programming style. We have a higher order function doing the heavy lifting of consuming the list; all we had to do was plug in the logic to aggregate results. The construction evidently terminates (you need only prove termination for foldr), and it's more efficient than the original hand-written version to boot.

Aside: @AlexR points out in the comments that zygomorphism has a big sister called mutumorphism, which captures mutual recursion in all its glory. mutu generalises zygo in that both the folding functions are allowed to inspect the other's result from the previous iteration.

mutuL :: (a -> b -> c -> b) ->
         (a -> b -> c -> c) ->
         b -> c ->
         [a] -> c
mutuL f g z e = snd . cataL (\x (p, q) -> (f x p q, g x p q)) (z, e)

You recover zygo from mutu simply by ignoring the extra argument. zygoL f = mutuL (\x p q -> f x p)


Of course, all of these folding patterns generalise from lists to the fixed point of an arbitrary functor:

newtype Fix f = Fix { unFix :: f (Fix f) }

cata :: Functor f => (f a -> a) -> Fix f -> a
cata f = f . fmap (cata f) . unFix

para :: Functor f => (f (Fix f, a) -> a) -> Fix f -> a
para f = snd . cata (\x -> (Fix $ fmap fst x, f x))

zygo :: Functor f => (f b -> b) -> (f (b, a) -> a) -> Fix f -> a
zygo f g = snd . cata (\x -> (f $ fmap fst x, g x))

mutu :: Functor f => (f (b, a) -> b) -> (f (b, a) -> a) -> Fix f -> a
mutu f g = snd . cata (\x -> (f x, g x))

Compare the definition of zygo with that of zygoL. Also note that zygo Fix = para, and that the latter three folds can be implemented in terms of cata. In foldology everything is related to everything else.

You can recover the list version from the generalised version.

data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)

zygoL' :: (a -> b -> b) -> (a -> b -> c -> c) -> b -> c -> List a -> c
zygoL' f g z e = zygo k l
    where k Nil_ = z
          k (Cons_ x y) = f x y
          l Nil_ = e
          l (Cons_ x (y, z)) = g x y z

pm4 = zygoL' (\_ p -> not p) (\x isEven total -> if isEven
                                                 then x - total
                                                 else x + total) True 0
Unseasoned answered 28/4, 2016 at 10:28 Comment(5)
I'll let someone else provide an answer about futumorphisms because I haven't quite been able to stuff them into my head yet.Unseasoned
Do I understand correctly then, that a zygomorphism is a special case of a mutumorphism?Asphyxiant
@AlexR Yep! mutu allows each function to depend on the result of the other, whereas zygo allows one function to depend on the result of the other. mutu generalises zygo.Unseasoned
Your use-case-based derivation of list-only zygo made it easy to understand the pattern and see when to use it. Thanks!Shere
One minor observation. In the examples that no longer traverse the list to determine even/odd, the isEven name is misleading. As written, it is convention whether ones starts the call with True or False (or one could traverse the list once before the call to determine). No big deal - and does not take away from the very useful info. Thanks.Shere
U
14

Histomorphism models dynamic programming, the technique of tabulating the results of previous subcomputations. (It's sometimes called course-of-value induction.) In a histomorphism, the folding function has access to a table of the results of earlier iterations of the fold. Compare this with the catamorphism, where the folding function can only see the result of the last iteration. The histomorphism has the benefit of hindsight - you can see all of history.

Here's the idea. As we consume the input list, the folding algebra will output a sequence of bs. histo will jot down each b as it emerges, attaching it to the table of results. The number of items in the history is equal to the number of list layers you've processed - by the time you've torn down the whole list, the history of your operation will have a length equal to that of the list.

This is what the history of iterating a list(ory) looks like:

data History a b = Ancient b | Age a b (History a b)

History is a list of pairs of things and results, with an extra result at the end corresponding to the []-thing. We'll pair up each layer of the input list with its corresponding result.

cataL = foldr

history :: (a -> History a b -> b) -> b -> [a] -> History a b
history f z = cataL (\x h -> Age x (f x h) h) (Ancient z)

Once you've folded up the whole list from right to left, your final result will be at the top of the stack.

headH :: History a b -> b
headH (Ancient x) = x
headH (Age _ x _) = x

histoL :: (a -> History a b -> b) -> b -> [a] -> b
histoL f z = headH . history f z

(It happens that History a is a comonad, but headH (née extract) is all we need to define histoL.)


History labels each layer of the input list with its corresponding result. The cofree comonad captures the pattern of labelling each layer of an arbitrary structure.

data Cofree f a = Cofree { headC :: a, tailC :: f (Cofree f a) }

(I came up with History by plugging ListF into Cofree and simplifying.)

Compare this with the free monad,

data Free f a = Free (f (Free f a))
              | Return a

Free is a coproduct type; Cofree is a product type. Free layers up a lasagne of fs, with values a at the bottom of the lasagne. Cofree layers up the lasagne with values a at each layer. Free monads are generalised externally-labelled trees; cofree comonads are generalised internally-labelled trees.

With Cofree in hand, we can generalise from lists to the fixpoint of an arbitrary functor,

newtype Fix f = Fix { unFix :: f (Fix f) }

cata :: Functor f => (f b -> b) -> Fix f -> b
cata f = f . fmap (cata f) . unFix

histo :: Functor f => (f (Cofree f b) -> b) -> Fix f -> b
histo f = headC . cata (\x -> Cofree (f x) x)

and once more recover the list version.

data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)
type History' a b = Cofree (ListF a) b

histoL' :: (a -> History' a b -> b) -> b -> List a -> b
histoL' f z = histo g
    where g Nil_ = z
          g (Cons_ x h) = f x h

Aside: histo is the dual of futu. Look at their types.

histo :: Functor f => (f (Cofree f a) -> a) -> (Fix f -> a)
futu  :: Functor f => (a  ->  f (Free f a)) -> (a -> Fix f)

futu is histo with the arrows flipped and with Free replaced by Cofree. Histomorphisms see the past; futumorphisms predict the future. And much like cata f . ana g can be fused into a hylomorphism, histo f . futu g can be fused into a chronomorphism.

Even if you skip the mathsy parts, this paper by Hinze and Wu features a good, example-driven tutorial on histomorphisms and their usage.

Unseasoned answered 3/5, 2016 at 11:27 Comment(0)
A
13

Since no one else has answered for futu yet, I'll try to stumble my way through. I'm going to use ListF a b = Base [a] = ConsF a b | NilF

Taking the type in recursion-schemes: futu :: Unfoldable t => (a -> Base t (Free (Base t) a)) -> a -> t.

I'm going to ignore the Unfoldable constraint and substitute [b] in for t.

(a -> Base [b] (Free (Base [b]) a)) -> a -> [b]
(a -> ListF b (Free (ListF b) a)) -> a -> [b]

Free (ListF b) a) is a list, possibly with an a-typed hole at the end. This means that it's isomorphic to ([b], Maybe a). So now we have:

(a -> ListF b ([b], Maybe a)) -> a -> [b]

Eliminating the last ListF, noticing that ListF a b is isomorphic to Maybe (a, b):

(a -> Maybe (b, ([b], Maybe a))) -> a -> [b]

Now, I'm pretty sure that playing type-tetris leads to the only sensible implementation:

futuL f x = case f x of
  Nothing -> []
  Just (y, (ys, mz)) -> y : (ys ++ fz)
    where fz = case mz of
      Nothing -> []
      Just z -> futuL f z

Summarizing the resulting function, futuL takes a seed value and a function which may produce at least one result, and possibly a new seed value if it produced a result.

At first I thought this was equivalent to

notFutuL :: (a -> ([b], Maybe a)) -> a -> [b]
notFutuL f x = case f x of
  (ys, mx) -> ys ++ case mx of
    Nothing -> []
    Just x' -> notFutuL f x'

And in practice, perhaps it is, more or less, but the one significant difference is that the real futu guarantees productivity (i.e. if f always returns, you will never be stuck waiting forever for the next list element).

Asphyxiant answered 28/4, 2016 at 19:14 Comment(3)
Your last point about guaranteeing productivity isn't quite right. It's more subtle than that. f certainly could loop forever, or throw an exception; there's no way to be sure in Haskell. The difference is that the second type allows f to return an a (and continue iteration) without returning any bs. In other words futu will always terminate if f does, whereas notFutu can be induced to loop by a terminating f.Unseasoned
How is your clarification different than my clarification (in the parenthetical)? Not trying to be antagonistic; just curious.Asphyxiant
Thanks! BTW: the recursive call to notFutuL is missing the f argument.Shere

© 2022 - 2024 — McMap. All rights reserved.