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?
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éralisezygo
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 demutu
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
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 b
s. 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 f
s, 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 defutu
. 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
esthisto
avec les flèches inversées et avecFree
remplacé parCofree
. Les histomorphismes voient le passé; les futumorphismes prédisent l'avenir. Et un peu commecata 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.
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).