web-dev-qa-db-fra.com

Histomorphismes, zygomorphismes et futumorphismes spécialisés dans les listes

J'ai fini par comprendre. Voir la vidéo et les diapositives d'une conférence que j'ai donnée:

Question d'origine:

Dans mes efforts pour comprendre les schémas de récursivité génériques (c'est-à-dire qui utilisent Fix), j'ai trouvé utile d'écrire des versions de liste uniquement des différents schémas. Cela rend beaucoup plus facile la compréhension des schémas réels (sans la surcharge supplémentaire des trucs Fix).

Cependant, je n'ai pas encore compris comment définir les versions de liste uniquement de zygo et futu.

Voici mes définitions spécialisées jusqu'à présent:

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)

Comment définissez-vous histo, zygo et futu pour les listes?

38
haroldcarr

Zygomorphisme est le nom mathématique hautement falutin que nous donnons aux plis construits à partir de deux semi-mutuellement fonctions récursives. Je vais vous donner un exemple.

Imaginez une fonction pm :: [Int] -> Int (Pour plus-moins) qui intercale + Et - Alternativement à travers une liste de nombres, telle que pm [v,w,x,y,z] = v - (w + (x - (y + z))). Vous pouvez l'écrire en utilisant la récursion primitive:

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

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

Il est clair que pm0 N'est pas compositionnel - vous devez inspecter la longueur de la liste entière à chaque position pour déterminer si vous ajoutez ou soustrayez. Paramorphisme modélise la récursion primitive de ce type, lorsque la fonction de repliement doit traverser tout le sous-arbre à chaque itération du repli. Ainsi, nous pouvons au moins réécrire le code pour se conformer à un modèle établi.

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

Mais c'est inefficace. lengthEven parcourt toute la liste à chaque itération du paramorphisme résultant en un O (n2) algorithme.


Nous pouvons progresser en notant que lengthEven et para peuvent être exprimés comme un catamorphisme avec foldr...

cataL = foldr

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

... ce qui suggère que nous pourrions être en mesure de fusionner les deux opérations en un seul passage sur la liste.

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

Nous avions un pli qui dépendait du résultat d'un autre pli, et nous avons pu les fusionner en une seule traversée de la liste. Le zygomorphisme capture exactement ce modèle.

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)

À chaque itération du pli, f voit sa réponse de la dernière itération comme dans un catamorphisme, mais g voit les réponses des deux fonctions. g s'emmêle avec f.

Nous allons écrire pm comme un zygomorphisme en utilisant la première fonction de pliage pour compter si la liste est paire ou impaire et la seconde pour calculer le total.

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

Il s'agit d'un style de programmation fonctionnel classique. Nous avons une fonction d'ordre supérieur faisant le gros du travail de consommer la liste; tout ce que nous avions à faire était de brancher la logique pour agréger les résultats. La construction se termine évidemment (il suffit de prouver la terminaison pour foldr), et elle est plus efficace que la version manuscrite d'origine pour démarrer.

À part : @AlexR souligne dans les commentaires que le zygomorphisme a une grande sœur appelée mutumorphisme, qui capture la récursion mutuelle dans toute sa gloire. mutu généralise zygo en ce que les deux les fonctions de pliage sont autorisées à inspecter le résultat de l'autre à partir de l'itération précédente.

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)

Vous récupérez zygo à partir de mutu simplement en ignorant l'argument supplémentaire. zygoL f = mutuL (\x p q -> f x p)


Bien sûr, tous ces modèles de pliage se généralisent à partir des listes jusqu'au point fixe d'un foncteur arbitraire:

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))

Comparez la définition de zygo avec celle de zygoL. Notez également que zygo Fix = para, Et que les trois derniers plis peuvent être implémentés en termes de cata. En pliologie, tout est lié à tout le reste.

Vous pouvez récupérer la version de liste à partir de la version généralisée.

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
39
Benjamin Hodgson

Histomorphisme modèles programmation dynamique, la technique de tabulation des résultats des sous-calculs précédents. (On l'appelle parfois induction de cours de valeur .) Dans un histomorphisme, la fonction de pliage a accès à un tableau des résultats des itérations antérieures du pli. Comparez cela avec le catamorphisme, où la fonction de pliage ne peut voir que le résultat de la dernière itération. L'histomorphisme a l'avantage du recul - vous pouvez voir toute l'histoire.

Voici l'idée. Au fur et à mesure que nous consommons la liste d'entrée, l'algèbre pliante affichera une séquence de bs. histo notera chaque b au fur et à mesure de son émergence, en l'attachant au tableau des résultats. Le nombre d'éléments dans l'historique est égal au nombre de couches de liste que vous avez traitées - au moment où vous avez supprimé toute la liste, l'historique de votre opération aura une longueur égale à celle de la liste.

Voici à quoi ressemble l'histoire de l'itération d'une liste (ory):

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

History est une liste de paires d'éléments et de résultats, avec un résultat supplémentaire à la fin correspondant à []-chose. Nous appairons chaque couche de la liste d'entrée avec son résultat correspondant.

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)

Une fois que vous avez replié toute la liste de droite à gauche, votre résultat final sera en haut de la pile.

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

(Il arrive que History a est un comonad , mais headH (née extract) est tout ce dont nous avons besoin pour définir histoL.)


History étiquette chaque couche de la liste d'entrée avec son résultat correspondant. Le comonad cofree capture le modèle d'étiquetage de chaque couche d'une structure arbitraire.

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

(J'ai trouvé History en branchant ListF dans Cofree et en simplifiant.)

Comparez cela avec la monade gratuite,

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

Free est un type de coproduit; Cofree est un type de produit. Free superpose une lasagne de fs, avec des valeurs a au bas de la lasagne. Cofree superpose les lasagnes avec des valeurs a à chaque couche. Les monades libres sont des arbres généralisés étiquetés à l'extérieur; les comonades cofree sont des arbres généralisés étiquetés en interne.

Avec Cofree en main, nous pouvons généraliser à partir de listes vers le point fixe d'un foncteur arbitraire,

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)

et une fois de plus récupérer la version liste.

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

Mis à part : histo est le double de futu. Regardez leurs types.

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

futu est histo avec les flèches inversées et avec Free remplacé par Cofree. Les histomorphismes voient le passé; les futumorphismes prédisent l'avenir. Et un peu comme cata f . ana g peut être fusionné en un hylomorphisme, histo f . futu g peut être fusionné en chronomorphisme .

Même si vous ignorez les parties mathématiques, cet article de Hinze et W propose un bon tutoriel basé sur des exemples sur les histomorphismes et leur utilisation.

12

Comme personne d'autre n'a encore répondu pour futu, je vais essayer de trébucher. Je vais utiliser ListF a b = Base [a] = ConsF a b | NilF

Prendre le type dans recursion-schemes : futu :: Unfoldable t => (a -> Base t (Free (Base t) a)) -> a -> t.

Je vais ignorer la contrainte Unfoldable et remplacer [b] Par t.

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

Free (ListF b) a) est une liste, éventuellement avec un trou de type a- à la fin. Cela signifie qu'il est isomorphe à ([b], Maybe a). Nous avons donc maintenant:

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

Éliminer le dernier ListF, en remarquant que ListF a b Est isomorphe à Maybe (a, b):

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

Maintenant, je suis presque sûr que jouer à tetris de type conduit à la seule implémentation sensée:

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

Pour résumer la fonction résultante, futuL prend une valeur de départ et une fonction qui peut produire au moins un résultat , et éventuellement une nouvelle valeur de départ si elle a produit un résultat.

Au début, je pensais que cela équivalait à

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'

Et dans la pratique, c'est peut-être plus ou moins, mais la seule différence significative est que le vrai futu garantit la productivité (c'est-à-dire si f revient toujours, vous ne serez jamais coincé à attendre indéfiniment le élément de liste suivant).

12
Alex R