Dans Real World Haskell, Chapitre 4. Programmation fonctionnelle
Ecrire 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 troublé, et quelqu'un appelé dps l'a récrit avec un nom explicite 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 donnant un exemple et en montrant le mécanisme sous-jacent pas à pas:
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 ne peux toujours pas comprendre cela, voici mes questions:
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 de l'implémentation de myFoldl utilise 3 paramètres. Je suis complètement confus!Est-ce qu'il y a quelqu'un qui peut m'aider? Merci beaucoup!
Quelques explications sont en ordre!
A quoi sert la fonction id? Quel est le rôle de? Pourquoi devrions-nous en avoir besoin ici?
id
est le fonction d'identité , id x = x
, et est utilisé comme équivalent à zéro lors de la création d'une chaîne de fonctions avec composition de fonctions , (.)
. Vous pouvez le trouver défini dans le prélude .
Dans l'exemple ci-dessus, id function est l'accumulateur de la fonction lambda?
L'accumulateur est une fonction en cours de création via une application de fonction répétée. Il n'y a pas de lambda explicite, puisque nous nommons l'accumulateur, step
. Vous pouvez l'écrire avec un lambda si vous voulez:
foldl f a bs = foldr (\b g x -> g (f x b)) id bs a
Ou comme Graham Hutton écrirait :
5.1 L'opérateur
foldl
Maintenant généralisons à partir de l'exemple
suml
et considérons l'opérateur standardfoldl
qui traite les éléments d'une liste dans un ordre de gauche à droite en utilisant une fonctionf
pour combiner des valeurs, et une valeurv
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 parsuml = foldl (+) 0
. Beaucoup d'autres fonctions peuvent être définies d'une manière simple en utilisantfoldl
. Par exemple, la fonction standardreverse
peut être redéfinie à l'aide defoldl
comme suit:reverse :: [α] → [α] reverse = foldl (λxs x → x : xs) [ ]
Cette définition est plus efficace que notre définition originale utilisant fold, car elle évite l’utilisation de l’opérateur d’ajout inefficace
(++)
pour les listes.Une simple généralisation du calcul dans la section précédente pour la fonction
suml
montre comment redéfinir la fonctionfoldl
en fonction defold
: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 defoldl
, carfoldl
est strict à la fin de son argument de liste maisfold
n’est pas . Il existe un certain nombre de "théorèmes de dualité" utiles concernantfold
etfoldl
, ainsi que quelques directives pour choisir l’opérateur le mieux adapté à des applications particulières (Bird, 1998).
Le prototype de est foldr :: (a -> b -> b) -> b -> [a] -> b .
Un programmeur Haskell dirait que le type de foldr
est (a -> b -> b) -> b -> [a] -> b
.
et le premier paramètre est une fonction qui nécessite deux paramètres, mais la fonction step de l'implémentation de myFoldl utilise trois paramètres. Je suis complètement confus
C'est déroutant et magique! Nous jouons un tour et remplaçons l'accumulateur par une fonction, qui est à son tour appliquée à la valeur initiale pour produire un résultat.
Graham Hutton explique le truc pour transformer 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 refactorisez-le 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
de manière à faire flotter les v
intérieurs:
foldl f v xs = g xs v
where
g [] = \v -> v
g (x:xs) = \v -> g xs (f v x)
Ce qui revient à penser que g
en fonction d'un argument 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 une liste de manière récursive, applique une fonction f
. La valeur finale est la fonction d'identité, et chaque étape entraîne également une fonction.
Mais , nous avons déjà en main une fonction récursive très similaire sur les listes, foldr
!
2 L'opérateur de pliage
L'opérateur
fold
tire ses origines de la théorie de la récursion (Kleene, 1952), tandis que l'utilisation defold
en tant que concept central dans un langage de programmation remonte à l'opérateur de réduction d'APL (Iverson, 1962). et plus tard à l'opérateur d'insertion de FP (Backus, 1978). Dans Haskell, l'opérateurfold
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, étant donné une fonction
f
de typeα → β → β
et une valeurv
de typeβ
, la fonctionfold 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 valeurv
, et chaque constructeur contre(:)
dans la liste par la fonctionf
. De cette manière, l'opérateurfold
encapsule un modèle simple de récursivité pour le traitement des listes, dans lequel les deux constructeurs des 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 utilisantfold
.
Cela ressemble à un schéma récursif très similaire à notre fonction g
. Maintenant, le truc: en utilisant toute la magie disponible (aka Bird, Meertens et Malcolm), nous appliquons une règle spéciale, la propriété universelle de fold , qui est une équivalence entre deux définitions pour une fonction g
qui traite des listes, ainsi:
g [] = v g (x:xs) = f x (g xs)
si et seulement si
g = fold f v
Ainsi, la propriété universelle des plis stipule que:
g = foldr k v
où g
doit être équivalent aux deux équations, pour certaines k
et v
:
g [] = v
g (x:xs) = k x (g xs)
De nos précédentes conceptions de foldl, nous connaissons v == id
. Pour la deuxième équation cependant, nous devons 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'
En remplaçant nos définitions calculées de k
et v
, on obtient une définition de foldl comme suit:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v xs =
foldr
(\x g -> (\a -> g (f v x)))
id
xs
v
Le g
récursif 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 se plie donc à gauche au lieu de ).
Ceci est certainement un peu avancé, donc pour comprendre en profondeur cette transformation, la propriété universelle de folds , qui rend la transformation possible, je recommande le tutoriel de Hutton, lié ci-dessous.
Références
Considérons 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
. Etant donné que 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 soyez pas dérouté par les différentes significations de a
dans différentes signatures; c'est juste une variable de type. Aussi, gardez à l'esprit que la flèche de fonction est associative à droite, donc a -> b -> c
est la même chose que a -> (b -> c)
.
Donc, oui, la valeur de l'accumulateur pour foldr
est une fonction de type a -> a
et la valeur initiale est id
. Cela a du sens, car id
est une fonction qui ne fait rien - c'est la même raison pour laquelle vous commenceriez avec zéro comme valeur initiale lors de l'ajout de toutes les valeurs d'une liste.
En ce qui concerne 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 cela rend plus facile de voir ce qui se passe? Il faut un paramètre supplémentaire car il renvoie une fonction et les deux manières de l'écrire sont équivalentes. Notez également le paramètre supplémentaire après la foldr
: (foldr step id xs) z
. La partie entre parenthèses est le pli lui-même, qui renvoie une fonction, qui est ensuite appliquée à z
.
(parcourez rapidement mes réponses [1] , [2] , [3] , [4] pour vous assurer de bien comprendre la syntaxe de Haskell, les fonctions d'ordre supérieur, currying, composition de fonction, opérateur $, opérateurs infixe/préfixe, sections et lambdas)
Un fold est simplement une codification de certains types de récursion. Et la propriété d'universalité indique simplement que, si votre récursivité se conforme à une certaine forme, elle peut être transformée en repliement selon certaines règles formelles. Et inversement, chaque pli peut être transformé en une récursion de ce genre. Encore une fois, certaines récursions peuvent être traduites en plis qui donnent exactement la même réponse, d'autres non, et il existe une procédure exacte pour le faire.
Fondamentalement, si votre fonction récursive fonctionne sur des listes et ressemble à celle du à gauche, vous pouvez la transformer pour plier le à droite en substituant f
et v
à ce qui existe réellement.
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
et sum (x:xs) = x + sum xs
sont équivalents à 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:
Implémentez
map
,filter
,reverse
,concat
etconcatMap
de manière récursive, tout comme les fonctions ci-dessus du côté {gauche}.Convertissez ces 5 fonctions en foldr selon une formule ci-dessus, c’est-à-dire en substituant
f
etv
dans la formule de pliage sur le droit.
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 à trouver se développe complètement avant même que l’ajout ne commence, 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 des nombres à chaque étape (en savoir plus sur tail récursivité pour en savoir plus sur les stratégies de récursivité):
suml :: [a] -> a
suml xs = suml' xs 0
where suml' [] n = n -- auxiliary function
suml' (x:xs) n = suml' xs (n+x)
Bon, arrête! Exécutez ce code dans GHCi et assurez-vous de bien comprendre son fonctionnement, puis procédez soigneusement et réfléchi. 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 fonction, non? Et v = suml' []
de la formule de la propriété universelle. Ensemble, cela donne v n = n
, une fonction qui renvoie immédiatement tout 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
Donc, suml' = foldr (\x g n -> g (n+x)) id
et, donc, 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, il suffit de 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
Maintenant, lisez Graham Hutton Un tutoriel sur l’universalité et l’expressivité du repli . Prenez un stylo et du papier, essayez de comprendre tout ce qu'il écrit jusqu'à ce que vous obteniez la plupart des plis par vous-même. Ne pas transpirer si vous ne comprenez pas quelque chose, vous pouvez toujours revenir plus tard, mais ne tardez pas beaucoup non plus.
Voici ma preuve que foldl
peut être exprimé en termes de foldr
, ce que je trouve assez simple à part le nom spaghetti que la fonction step
introduit.
La proposition est que 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é comme
(foldr step_f id xs) z
puisque foldr
ne prend que trois paramètres. Cela laisse déjà entendre que la foldr
calculera non pas une valeur mais une fonction curry, qui est ensuite appliquée à z
. Il faut enquêter sur deux cas pour savoir si myfoldl
est foldl
:
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)
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
Comme dans 2. la première et la dernière ligne ont la même forme dans les deux cas, il est possible de replier la liste jusqu'à xs == []
, auquel cas 1. garantit le même résultat. Donc, par induction, myfoldl == foldl
.
Il n’ya pas de voie royale vers les mathématiques, ni même par Haskell. Laisser
h z = (foldr step id xs) z where
step x g = \a -> g (f a x)
Qu'est-ce que c'est que h z
? Supposons que xs = [x0, x1, x2]
.
Appliquer la définition de foldr:
h z = (step x0 (step x1 (step x2 id))) z
Appliquer la définition de l'étape:
= (\a0 -> (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f a0 x0)) z
Remplacez les fonctions lambda:
= (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f z x0)
= (\a2 -> id (f a2 x2)) (f (f z x0) x1)
= id (f (f (f z x0) x1) x2)
Appliquer la définition de l'identifiant:
= f (f (f z x0) x1) x2
Appliquer la définition de foldl:
= foldl f z [x0, x1, x2]
Est-ce une route royale ou quoi?
Cela pourrait aider, j'ai essayé de développer d'une manière différente.
myFoldl (+) 0 [1,2,3] =
foldr step id [1,2,3] 0 =
foldr step (\a -> id (a+3)) [1,2] 0 =
foldr step (\b -> (\a -> id (a+3)) (b+2)) [1] 0 =
foldr step (\b -> id ((b+2)+3)) [1] 0 =
foldr step (\c -> (\b -> id ((b+2)+3)) (c+1)) [] 0 =
foldr step (\c -> id (((c+1)+2)+3)) [] 0 =
(\c -> id (((c+1)+2)+3)) 0 = ...
foldr step zero (x:xs) = step x (foldr step zero xs)
foldr _ zero [] = zero
myFold f z xs = foldr step id xs z
where step x g a = g (f a x)
myFold (+) 0 [1, 2, 3] =
foldr step id [1, 2, 3] 0
-- Expanding foldr function
step 1 (foldr step id [2, 3]) 0
step 1 (step 2 (foldr step id [3])) 0
step 1 (step 2 (step 3 (foldr step id []))) 0
-- Expanding step function if it is possible
step 1 (step 2 (step 3 id)) 0
step 2 (step 3 id) (0 + 1)
step 3 id ((0 + 1) + 2)
id (((0 + 1) + 2) + 3)
Eh bien, au moins, cela m'a aidé. Même ce n'est pas tout à fait correct.