80 votes

Écriture de foldl à l'aide de foldr

Sur Haskell dans le monde réel , chapitre 4. sur Programmation fonctionnelle :

Écrire foldl avec foldr :

-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a

myFoldl f z xs = foldr step id xs z
    where step x g a = g (f a x)

Le code ci-dessus m'a beaucoup perturbé, et quelqu'un a appelé dps Je l'ai réécrit avec un nom significatif pour le rendre un peu plus clair :

myFoldl stepL zeroL xs = (foldr stepR id xs) zeroL
where stepR lastL accR accInitL = accR (stepL accInitL lastL)

Quelqu'un d'autre, Jef G, a ensuite fait un excellent travail en fournissant un exemple et en montrant le mécanisme sous-jacent étape par étape :

myFoldl (+) 0 [1, 2, 3]
= (foldR step id [1, 2, 3]) 0
= (step 1 (step 2 (step 3 id))) 0
= (step 1 (step 2 (\a3 -> id ((+) a3 3)))) 0
= (step 1 (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2))) 0
= (\a1 -> (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (\a3 -> (+) a3 3) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (+) ((+) a2 2) 3) ((+) a1 1)) 0
= (\a1 -> (+) ((+) ((+) a1 1) 2) 3) 0
= (+) ((+) ((+) 0 1) 2) 3
= ((0 + 1) + 2) + 3

Mais je n'arrive toujours pas à le comprendre pleinement, voici mes questions :

  1. A quoi sert la fonction id ? À quoi sert la fonction id ? Pourquoi en aurions-nous besoin ici ?
  2. Dans l'exemple ci-dessus, la fonction id est l'accumulateur dans la fonction lambda ?
  3. Le prototype de foldr est foldr :: (a -> b -> b) -> b -> [a] -> b et le premier paramètre est une fonction qui nécessite deux paramètres, mais la fonction step dans l'implémentation de myFoldl utilise 3 paramètres, je suis complètement perdu !

2 votes

Pour les vrais masochistes, step = curry $ uncurry (&) <<< (flip f) *** (.)

105voto

Don Stewart Points 94361

Quelques explications s'imposent !

A quoi sert la fonction id ? À quoi sert la fonction id ? Pourquoi en aurions-nous besoin ici ?

id est le fonction d'identité , id x = x et est utilisé comme l'équivalent du zéro lors de la construction d'une chaîne de fonctions à l'aide de la fonction composition des fonctions , (.) . Vous pouvez le trouver défini dans le prélude .

Dans l'exemple ci-dessus, la fonction id est l'accumulateur dans la fonction lambda ?

L'accumulateur est une fonction qui se construit par l'application répétée d'une fonction. Il n'y a pas de lambda explicite, puisque nous nommons l'accumulateur, step . Vous pouvez l'écrire avec un lambda si vous le souhaitez :

foldl f a bs = foldr (\b g x -> g (f x b)) id bs a

Ou comme Graham Hutton écrirait :

5.1 Le foldl opérateur

Maintenant, généralisons à partir de la suml exemple et considérer l'opérateur standard foldl qui traite les éléments d'une liste dans l'ordre de gauche à droite en utilisant une fonction f pour combiner des valeurs, et une valeur v comme valeur de départ :

foldl :: (    )    ([]  )
foldl f v [ ] = v
foldl f v (x : xs) = foldl f (f v x) xs

En utilisant cet opérateur, suml peut être redéfini simplement par suml = foldl (+) 0 . De nombreuses autres fonctions peuvent être définies de manière simple à l'aide de foldl . Par exemple, la fonction standard reverse peut être redéfini en utilisant foldl comme suit :

reverse :: []  []
reverse = foldl (xs x  x : xs) [ ]

Cette définition est plus efficace que notre définition originale utilisant le pliage, car elle évite l'utilisation de l'opérateur inefficace append. (++) pour les listes.

Une simple généralisation du calcul de la section précédente pour la fonction suml montre comment redéfinir la fonction foldl en termes de fold :

foldl f v xs = fold (x g  (a  g (f a x))) id xs v

En revanche, il n'est pas possible de redéfinir fold en termes de foldl en raison du fait que foldl est strict dans la queue de son argument liste mais fold ne l'est pas. Il existe un certain nombre de "théorèmes de dualité" utiles concernant fold y foldl ainsi que quelques lignes directrices pour décider quel opérateur est le mieux adapté à des applications particulières (Bird, 1998).

Le prototype de foldr est foldr : : (a -> b -> b) -> b -> [a] -> b

Un programmeur Haskell dirait que l'élément type de foldr es (a -> b -> b) -> b -> [a] -> b .

et le premier paramètre est une fonction qui a besoin de deux paramètres, mais la fonction étape dans l'implémentation de myFoldl utilise 3 paramètres, je suis complètement confus.

C'est déroutant et magique ! On joue un tour et on remplace l'accumulateur par une fonction, qui est à son tour appliquée à la valeur initiale pour donner un résultat.

Graham Hutton explique le truc pour tourner foldl en foldr dans l'article ci-dessus. Nous commençons par écrire une définition récursive de foldl :

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v []       = v
foldl f v (x : xs) = foldl f (f v x) xs

Et ensuite le refactoriser via la transformation d'argument statique sur f :

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs = g xs v
    where
        g []     v = v
        g (x:xs) v = g xs (f v x)

Réécrivons maintenant g afin de faire flotter le v vers l'intérieur :

foldl f v xs = g xs v
    where
        g []     = \v -> v
        g (x:xs) = \v -> g xs (f v x)

Ce qui revient à penser à g comme une fonction à un argument, qui renvoie une fonction :

foldl f v xs = g xs v
    where
        g []     = id
        g (x:xs) = \v -> g xs (f v x)

Maintenant, nous avons g une fonction qui parcourt récursivement une liste, appliquer une fonction f . La valeur finale est la fonction d'identité, et chaque étape donne également lieu à une fonction.

Mais nous avons déjà à portée de main une fonction récursive très similaire sur les listes, foldr !

2 L'opérateur de pliage

El fold a ses origines dans la théorie de la récursion (Kleene, 1952), tandis que l'utilisation de l'opérateur de fold comme concept central dans un langage de programmation remonte à l'opérateur de réduction de l'APL (Iverson, 1962), et plus tard à l'opérateur d'insertion du FP (Backus, 1978). En Haskell, l'opérateur fold pour les listes peut être défini comme suit :

fold :: (    )    ([]  )
fold f v [ ] = v
fold f v (x : xs) = f x (fold f v xs)

C'est-à-dire qu'étant donné une fonction f de type et une valeur v de type la fonction fold f v traite une liste de type [] pour donner une valeur de type en remplaçant le constructeur nil [] à la fin de la liste par la valeur v et chaque constructeur de cons (:) dans la liste par la fonction f . De cette manière, le fold encapsule un modèle simple de récursion pour le traitement des listes, dans lequel les deux constructeurs de listes sont simplement remplacés par d'autres valeurs et fonctions. Un certain nombre de fonctions familières sur les listes ont une définition simple utilisant l'opérateur fold .

Cela ressemble à un schéma récursif très similaire à celui de notre g fonction. Maintenant, l'astuce : en utilisant toute la magie disponible (aka Bird, Meertens et Malcolm), nous appliquons une règle spéciale, le propriété universelle du pli qui est une équivalence entre deux dénitions pour une fonction g qui traite des listes, énoncées comme :

g [] = v
g (x:xs) = f x (g xs)

si et seulement si

g = fold f v

Donc, la propriété universelle des plis stipule que :

    g = foldr k v

g doit être équivalente aux deux équations, pour certaines k y v :

    g []     = v
    g (x:xs) = k x (g xs)

D'après nos conceptions précédentes de foldl, nous savons v == id . Pour la deuxième équation cependant, nous avons besoin de calculer la définition de k :

    g (x:xs)         = k x (g xs)        
<=> g (x:xs) v       = k x (g xs) v      -- accumulator of functions
<=> g xs (f v x)     = k x (g xs) v      -- definition of foldl
<=  g' (f v x)       = k x g' v          -- generalize (g xs) to g'
<=> k = \x g' -> (\a -> g' (f v x))      -- expand k. recursion captured in g'

Ce qui, en substituant nos définitions calculées de k y v donne une définition de foldl comme :

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs =
    foldr
        (\x g -> (\a -> g (f v x)))
        id
        xs
        v

La récursive g est remplacé par le combinateur foldr, et l'accumulateur devient une fonction construite via une chaîne de compositions de f à chaque élément de la liste, dans l'ordre inverse (on plie donc à gauche au lieu de droite).

C'est certainement un peu avancé, donc pour comprendre en profondeur cette transformation, les propriété universelle des plis Je recommande le tutoriel de Hutton, dont le lien figure ci-dessous.


Références

1 votes

Veuillez corriger la faute de frappe dans k = \x g' -> (\a -> g' (f v x)) y (\x g -> (\a -> g (f v x)))

0 votes

La fonction finale semble erronée mais je ne la comprends pas assez bien pour en être complètement sûr. Je présume que (\x g -> (\a -> g (f v x))) devrait être remplacé par (\x g -> (\a -> g (f a x))) .

11voto

C. A. McCann Points 56834

Considérez le type de foldr :

foldr :: (b -> a -> a) -> a -> [b] -> a

Considérant que le type de step est quelque chose comme b -> (a -> a) -> a -> a . Puisque l'étape est passée à foldr nous pouvons conclure que dans ce cas le pli a un type comme (b -> (a -> a) -> (a -> a)) -> (a -> a) -> [b] -> (a -> a) .

Ne vous laissez pas déconcerter par les différentes significations des termes suivants a dans différentes signatures ; il s'agit simplement d'une variable de type. Gardez également à l'esprit que la fonction flèche est associative à droite, donc a -> b -> c est la même chose que a -> (b -> c) .

Donc, oui, la valeur de l'accumulateur pour l'élément foldr es un fonction de type a -> a et la valeur initiale est id . Cela a un certain sens, car id est une fonction qui ne fait rien - c'est la même raison pour laquelle vous commencez par zéro comme valeur initiale lorsque vous additionnez toutes les valeurs d'une liste.

Quant à step en prenant trois arguments, essayez de le réécrire comme ceci :

step :: b -> (a -> a) -> (a -> a)
step x g = \a -> g (f a x)

Est-ce que ça rend plus facile de voir ce qui se passe ? Elle prend un paramètre supplémentaire parce qu'elle renvoie une fonction, et les deux façons de l'écrire sont équivalentes. Notez également le paramètre supplémentaire après le foldr : (foldr step id xs) z . La partie entre parenthèses est le pli lui-même, qui renvoie une fonction, laquelle est ensuite appliquée à z .

6voto

sindikat Points 3533

(parcourir rapidement mes réponses [1] , [2] , [3] , [4] pour s'assurer que vous comprenez la syntaxe de Haskell, les fonctions d'ordre supérieur, le curry, la composition de fonctions, l'opérateur $, les opérateurs infixes/préfixes, les sections et les lambdas)

Propriété universelle du pli

A pliage est juste une codification de certains types de récurrence. Et la propriété d'universalité dit simplement que, si votre récursion se conforme à une certaine forme, elle peut être transformée en pli selon certaines règles formelles. Et inversement, chaque pli peut être transformé en une récursion de ce type. Encore une fois, certaines récursions peuvent être traduites en plis qui donnent exactement la même réponse, et certaines récursions ne le peuvent pas, et il existe une procédure exacte pour le faire.

Fondamentalement, si votre fonction récursive fonctionne sur des listes et ressemble à la fonction gauche vous pouvez le transformer en un pli de la droite en substituant f y v pour ce qui est réellement là.

g []     = v              
g (x:xs) = f x (g xs)          g = foldr f v

Par exemple :

sum []     = 0   {- recursion becomes fold -}
sum (x:xs) = x + sum xs        sum = foldr 0 (+)

Ici v = 0 y sum (x:xs) = x + sum xs est équivalent à sum (x:xs) = (+) x (sum xs) donc f = (+) . 2 autres exemples

product []     = 1
product (x:xs) = x * product xs    product = foldr 1 (*)

length []     = 0
length (x:xs) = 1 + length xs      length = foldr (\_ a -> 1 + a) 0

Exercice :

  1. Mettre en œuvre map , filter , reverse , concat y concatMap de manière récursive, tout comme les fonctions ci-dessus sur le fichier gauche côté.

  2. Convertir ces 5 fonctions en foldr selon la formule ci-dessus c'est-à-dire en substituant f y v dans la formule de pliage sur le droite .

Foldl via foldr

Comment écrire une fonction récursive qui additionne les nombres de gauche à droite ?

sum [] = 0     -- given `sum [1,2,3]` expands into `(1 + (2 + 3))`
sum (x:xs) = x + sum xs

La première fonction récursive qui se présente se développe complètement avant même de commencer à s'additionner, ce n'est pas ce dont nous avons besoin. Une approche consiste à créer une fonction récursive qui a accumulateur qui additionne immédiatement les chiffres à chaque étape (lire à ce sujet récursion de queue pour en savoir plus sur les stratégies de récursion) :

suml :: [a] -> a
suml xs = suml' xs 0
  where suml' [] n = n   -- auxiliary function
        suml' (x:xs) n = suml' xs (n+x)

Très bien, arrêtez ! Exécutez ce code dans GHCi et assurez-vous que vous comprenez comment il fonctionne, puis continuez avec soin et réflexion. suml ne peut pas être redéfini avec un pli, mais suml' peut l'être.

suml' []       = v    -- equivalent: v n = n
suml' (x:xs) n = f x (suml' xs) n

suml' [] n = n de la définition de la fonction, n'est-ce pas ? Et v = suml' [] de la formule de propriété universelle. Ensemble, cela donne v n = n une fonction qui renvoie immédiatement ce qu'elle reçoit : v = id . Calculons f :

suml' (x:xs) n = f x (suml' xs) n
-- expand suml' definition
suml' xs (n+x) = f x (suml' xs) n
-- replace `suml' xs` with `g`
g (n+x)        = f x g n

Ainsi, suml' = foldr (\x g n -> g (n+x)) id et, ainsi, suml = foldr (\x g n -> g (n+x)) id xs 0 .

foldr (\x g n -> g (n + x)) id [1..10] 0 -- return 55

Maintenant nous devons juste généraliser, remplacer + par une fonction variable :

foldl f a xs = foldr (\x g n -> g (n `f` x)) id xs a
foldl (-) 10 [1..5] -- returns -5

Conclusion

Lisez maintenant l'article de Graham Hutton Un tutoriel sur l'universalité et l'expressivité du pliage . Prenez un stylo et du papier, essayez de comprendre tout ce qu'il écrit jusqu'à ce que vous arriviez à dériver la plupart des plis par vous-même. Ne vous inquiétez pas si vous ne comprenez pas quelque chose, vous pourrez toujours revenir plus tard, mais ne procrastinez pas trop non plus.

5voto

David Points 2152

Voici ma preuve que foldl peut être exprimée en termes de foldr que je trouve assez simple, à part le nom "spaghetti". step introduit la fonction.

La proposition est la suivante foldl f z xs est équivalent à

myfoldl f z xs = foldr step_f id xs z
        where step_f x g a = g (f a x)

La première chose importante à noter ici est que le côté droit de la première ligne est en fait évalué en tant que

(foldr step_f id xs) z

depuis foldr ne prend que trois paramètres. Cela laisse déjà entrevoir que le foldr ne calculera pas une valeur mais une fonction curée, qui sera ensuite appliquée à z . Il y a deux cas à étudier pour savoir si myfoldl es foldl :

  1. Cas de base : liste vide

      myfoldl f z []
    = foldr step_f id [] z    (by definition of myfoldl)
    = id z                    (by definition of foldr)
    = z
    
      foldl f z []
    = z                       (by definition of foldl)
  2. Liste non vide

      myfoldl f z (x:xs)
    = foldr step_f id (x:xs) z          (by definition of myfoldl)
    = step_f x (foldr step_f id xs) z   (-> apply step_f)
    = (foldr step_f id xs) (f z x)      (-> remove parentheses)
    = foldr step_f id xs (f z x)
    = myfoldl f (f z x) xs              (definition of myfoldl)
    
      foldl f z (x:xs)
    = foldl f (f z x) xs

Puisque dans 2. la première et la dernière ligne ont la même forme dans les deux cas, on peut l'utiliser pour rabattre la liste jusqu'à ce que xs == [] et dans ce cas, 1. garantit le même résultat. Donc par induction, myfoldl == foldl .

2voto

Avant de procéder au downvoting, veuillez lire le paragraphe suivant

Je publie la réponse pour les personnes qui pourraient trouver cette approche mieux adaptée à leur façon de penser. La réponse contient peut-être des informations et des réflexions redondantes, mais c'est ce dont j'avais besoin pour m'attaquer au problème. De plus, comme il s'agit d'une autre réponse à la même question, il est évident qu'elle comporte des chevauchements importants avec les autres réponses, mais elle raconte comment j'ai pu appréhender ce concept.

C'est pourquoi j'ai commencé à rédiger ces notes comme un compte rendu personnel de mes réflexions en essayant de comprendre ce sujet. Il m'a fallu toute la journée pour toucher le cœur du sujet, si je l'ai vraiment compris.

Mon long chemin pour comprendre cet exercice simple

Partie facile : que devons-nous déterminer ?

Que se passe-t-il avec l'exemple d'appel suivant

foldl f z [1,2,3,4]

peut être visualisé avec le diagramme suivant (qui est sur Wikipedia mais je l'ai vu pour la première fois sur une autre réponse ):

          _____results in a number
         /
        f          f (f (f (f z 1) 2) 3) 4
       / \
      f   4        f (f (f z 1) 2) 3
     / \
    f   3          f (f z 1) 2
   / \
  f   2            f z 1
 / \
z   1

(A titre d'information, lorsque vous utilisez foldl chaque application de f n'est pas exécutée, et les expressions sont calculées de la manière dont je les ai écrites plus haut ; en principe, elles pourraient être calculées de bas en haut, et c'est exactement ce qu'a fait foldl' fait.)

L'exercice nous met essentiellement au défi d'utiliser foldr au lieu de foldl en changeant de manière appropriée la fonction de pas (nous utilisons donc s au lieu de f ) et l'accumulateur initial (nous utilisons donc ? au lieu de z ) ; la liste reste la même, sinon de quoi parlons-nous ?

L'appel à foldr doit ressembler à ça :

foldr s ? [1,2,3,4]

et le diagramme correspondant est le suivant :

    _____what does the last call return?
   /
  s
 / \
1   s
   / \
  2   s
     / \
    3   s
       / \
      4   ? <--- what is the initial accumulator?

L'appel aboutit à

s 1 (s 2 (s 3 (s 4 ?)))

Quels sont les s y ? ? Et quels sont leurs types ? On dirait que s il s'agit d'une fonction à deux arguments, un peu comme la fonction f mais ne tirons pas de conclusions hâtives. Aussi, laissons ? de côté pour un moment, et observons que z doit entrer en jeu dès que 1 entre en jeu ; cependant, comment z entrent en jeu dans l'appel au peut-être deux arguments s à savoir dans l'appel s 1 (…) ? Nous pouvons résoudre cette partie de l'énigme en choisissant une s qui prend 3 arguments, plutôt que les 2 que nous avons mentionnés précédemment, de sorte que l'appel le plus externe s 1 (…) résultera en une fonction prenant un argument, que nous pouvons passer z à !

Cela signifie que nous voulons l'appel original, qui se développe en

f (f (f (f z 1) 2) 3) 4

pour être équivalent à

s 1 (s 2 (s 3 (s 4 ?))) z

ou, en d'autres termes, nous voulons la fonction partiellement appliquée

s 1 (s 2 (s 3 (s 4 ?)))

pour être équivalent à la fonction lambda suivante

(\z -> f (f (f (f z 1) 2) 3) 4)

Encore une fois, les "seules" pièces dont nous avons besoin sont s y ? .

Point d'inflexion : reconnaître la composition de la fonction

Redessinons le diagramme précédent et écrivons à droite ce que nous voulons que chaque appel à s être équivalent :

  s          s 1 (…) == (\z -> f (f (f (f z 1) 2) 3) 4)
 / \
1   s        s 2 (…) == (\z -> f (f (f    z    2) 3) 4)
   / \
  2   s      s 3 (…) == (\z -> f (f       z       3) 4)
     / \
    3   s    s 4  ?  == (\z -> f          z          4)
       / \
      4   ? <--- what is the initial accumulator?

J'espère qu'il est clair, d'après la structure du diagramme, que la (…) sur chaque ligne est le côté droit de la ligne qui la suit ; mieux, c'est la fonction retournée par l'appel précédent (ci-dessous) à s .

Il devrait également être clair qu'un appel à s avec des arguments x y y est l'application (complète) de y à l'application partielle de f au seul argument x . Puisque l'application partielle de f a x peut être écrit comme le lambda (\z -> f z x) en appliquant pleinement y donne lieu à la lambda (\z -> y (f z x)) que je réécrirais dans ce cas comme suit y . (\z -> f z x) ; traduire les mots en une expression pour s on obtient

s x y = y . (\z -> f z x)

(C'est la même chose que s x y z = y (f z x) qui est le même que celui du livre, si vous renommez les variables).

Le dernier point est : quel est le initial "valeur" ? de l'accumulateur ? Le schéma ci-dessus peut être réécrit en développant les appels imbriqués pour en faire des chaînes de composition :

  s          s 1 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1)
 / \
1   s        s 2 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2)
   / \
  2   s      s 3 (…) == (\z -> f z 4) . (\z -> f z 3)
     / \
    3   s    s 4  ?  == (\z -> f z 4)
       / \
      4   ? <--- what is the initial accumulator?

Nous voyons ici que s simplement "empiler" des applications partielles successives de f pero el y en s x y = y . (\z -> f z x) suggère que l'interprétation de s 4 ? (et, à son tour, toutes les autres) manque une fonction principale à composer avec le lambda le plus à gauche.

C'est juste notre ? : il est temps de lui donner une raison d'être, en plus d'occuper une place dans l'appel à foldr . Que peut-on choisir pour ne pas modifier les fonctions résultantes ? Réponse : id El élément identitaire par rapport à l'opérateur de composition (.) .

  s          s 1 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1)
 / \
1   s        s 2 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2)
   / \
  2   s      s 3 (…) == id . (\z -> f z 4) . (\z -> f z 3)
     / \
    3   s    s 4 id  == id . (\z -> f z 4)
       / \
      4   id

La fonction recherchée est donc

myFoldl f z xs = foldr (\x g a -> g (f a x)) id xs z

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X