• Aucun résultat trouvé

À l’époque où nous faisions de la récursivité, nous avions repéré un thème récurrent dans nos fonctions récursives qui opéraient sur des listes. Nous introduisions un motif x:xs et nous faisions quelque chose avec la tête et quelque chose avec la queue. Il s’avère que c’est un motif très commun, donc il existe quelques fonctions très utiles qui l’encapsulent. Ces fonctions sont appelées des folds (NDT : des plis). Elles sont un peu comme la fonction map, seulement qu’elles réduisent une liste à une simple valeur.

Un fold prend une fonction binaire, une valeur de départ (que j’aime appeler l’accumulateur) et une liste à plier. La fonction binaire prend deux paramètres. Elle est appelée avec l’accumulateur en première ou deuxième position, et le premier ou le dernier élément de la liste comme autre paramètre, et produit un nouvel accumulateur. La fonction est appelée à nouveau avec le nouvel accumulateur et la nouvelle extrémité de la queue, et ainsi de suite. Une fois qu’on a traversé toute la liste, seul l’accumulateur reste, c’est la valeur à laquelle on a réduit la liste.

D’abord, regardons la fonction foldl, aussi appelée left fold (pli à gauche). Elle plie la liste en partant de la gauche. La fonction binaire est appelée avec la valeur de départ de l’accumulateur et la tête de liste. Cela produit un nouvel accumulateur, et la fonction est à nouveau appelée sur cette valeur et le prochain

élément de la liste, etc.

Implémentons sum à nouveau, mais cette fois, utilisons un fold plutôt qu’une récursivité explicite.

sum' :: (Num a) => [a] -> a

sum' xs = foldl (\acc x -> acc + x) 0 xs

Un, deux, trois, test :

ghci> sum' [3,5,2,1] 11

Regardons de près comment ce pli se déroule. \acc x -> acc + x est la fonction binaire. 0 est la valeur de départ et xs la liste à plier. D’abord, 0 est utilisé pour acc et 3 pour x. 0 + 3 retourne 3 et devient, pour ainsi dire, le nouvel accumulateur. Ensuite, 3 est utilisé comme accumulateur, et l’élément courant, 5 , résultant en un 8 comme nouvel accumulateur. Encore en avant, 8 est l’accumulateur, 2 la valeur courante, le nouvel accumulateur est donc 10. Utilisé avec la valeur courante 1, il produit 11. Bravo, vous venez d’achever votre premier pli !

Le diagramme professionnel sur la gauche illustre la façon dont le pli se déroule, étape par étape (jour après jour !). Le nombre vert kaki est l’accumulateur. Vous pouvez voir comme la liste est consommée par la gauche par l’accumulateur. Om nom nom nom ! (NDT : “Miam miam miam !”) Si on prend en compte le fait que les fonctions sont curryfiées, on peut écrire cette implémentation encore plus rapidement :

sum' :: (Num a) => [a] -> a

sum' = foldl (+) 0

La lambda (\acc x -> acc + x) est équivalente à (+). On peut omettre le xs à la fin parce que foldl (+) 0 retourne une fonction qui attend une liste. En général, lorsque vous avec une fonction foo a = bar b a, vous pouvez réécrire foo = bar b, grâce à la curryfication.

Bon, implémentons une autre fonction avec un pli à gauche avant de passer aux plis à droite. Je suis sûr que vous savez tous qu’ elem vérifie si une valeur fait partie d’une liste, donc je ne vais pas vous le rappeler (oups, je viens de le faire !). Implémentons-là avec un pli à gauche.

elem' :: (Eq a) => a -> [a] -> Bool

elem' y ys = foldl (\acc x -> if x == y then True else acc) False ys

Bien, bien, bien, qu’avons-nous là ? La valeur de départ et l’accumulateur sont de type booléen. Le type de l’accumulateur et de l’initialisateur est toujours le même quand on plie. Rappelez-vous en quand vous ne savez plus quoi utiliser comme valeur de départ, ça vous mettra sur la piste. Ici, on commence avec

False . Il est logique d’utiliser False comme valeur initiale. On présume que l’élément n’est pas là, tant qu’on n’a pas de preuve de sa présence. Notez que si l’on appelle fold sur une liste vide, on obtient en retour la valeur initiale. Ensuite, on vérifie le premier élément pour savoir si c’est celui que l’on cherche. Si c’est le cas, on passe l’accumulateur à True. Sinon, on ne touche pas à l’accumulateur. S’il était False, il reste à False car on ne vient pas de trouver l’élément. S’il était True, on le laisse aussi.

Le pli à droite, foldr travaille de manière analogue au pli à gauche, mais l’accumulateur consomme les valeurs en partant de la droite. Aussi, la fonction binaire du pli gauche prend l’accumulateur en premier paramètre, et la valeur courante en second ( \acc x -> …), celle du pli droit prend la valeur courante en premier et l’accumulateur en second ( \x acc -> …). C’est assez logique que le pli à droite ait l’accumulateur à droite, vu qu’il plie depuis la droite.

La valeur de l’accumulateur (et donc le résultat) d’un pli peut être de n’importe quel type. Un nombre, un booléen, ou même une liste. Implémentons la fonction map à l’aide d’un pli à droite. L’accumulateur sera la liste, on va accumuler les valeurs après mappage, élément par élément. De fait, il est évident que l’élément de départ sera une liste vide.

map' :: (a -> b) -> [a] -> [b]

map' f xs = foldr (\x acc -> f x : acc) [] xs

Si l’on mappe (+3) à [1, 2, 3], on attaque la liste par la droite. On prend le dernier élément, 3 et on applique la fonction, résultant en un 6. On le place à l’avant de l’accumulateur, qui était []. 6:[] est [6], et notre nouvel accumulateur. On applique (+3) sur 2, donnant 5 et on le place devant ( :)

l’accumulateur, l’accumulateur devient [5, 6]. On applique (+3) à 1 et on le place devant l’accumulateur, ce qui donne pour valeur finale [4, 5, 6].

Bien sûr, nous aurions pu implémenter cette fonction avec un pli gauche aussi. Cela serait map' f xs = foldl (\acc x -> acc ++ [f x]) [] xs, mais le problème, c’est que ++ est beaucoup plus coûteux que :, donc généralement, on utilise des plis à droite lorsqu’on construit des nouvelles listes à partir d’une liste.

Si vous renversez une liste, vous pouvez faire un pli droit sur une liste comme vous auriez fait un pli gauche sur la liste originale, et vice versa. Parfois, vous n’avez même pas besoin de ça. La fonction sum peut être implémentée aussi bien avec un pli à gauche qu’à droite. Une des grosses différences est que les plis à droite fonctionnent sur des listes infinies, alors que les plis à gauche, non ! Pour mettre cela au clair, si vous prenez un endroit d’une liste infinie et que vous vous mettez à plier depuis la droite depuis celui-ci, vous finirez par atteindre le début de la liste. Par contre, si vous vous placez à un endroit d’une liste infinie, et que vous commencez à plier depuis la gauche vers la droite, vous n’atteindrez jamais la fin !

Les plis peuvent être utilisés pour implémenter n’importe quelle fonction qui traverse une liste une fois, élément par élément, et retourne quoi que ce soit basé là-dessus. Si jamais vous voulez parcourir une liste

pour retourner quelque chose, vous aurez besoin d’un pli. C’est pourquoi les plis sont, avec les maps et les filtres, un des types les plus utiles en programmation fonctionnelle.

Les fonctions foldl1 et foldr1 fonctionnent quasiment comme foldl et foldr, mais n’ont pas besoin d’une valeur de départ. Elles considèrent que le

premier (ou le dernier respectivement) élément de la liste est la valeur de départ, et commencent à plier à partir de l’élément suivant. Avec cela en tête, la fonction sum peut être implémentée : sum = foldl1 (+) . Puisqu’elles dépendent du fait que les listes qu’elles essaient de plier aient au moins un élément, elles

peuvent provoquer des erreurs à l’exécution si on les appelle sur des listes vides. foldl et foldr, d’un autre côté, fonctionnent bien sur des listes vides. Quand vous faites un pli, pensez donc à la façon dont il se comporte sur une liste vide. Si la fonction n’a aucun sens pour une liste vide, vous pouvez probablement utiliser foldl1 et foldr1 pour l’implémenter.

Histoire de vous montrer la puissance des plis, on va implémenter un paquet de fonctions de la bibliothèque standard avec eux :

maximum' :: (Ord a) => [a] -> a

maximum' = foldr1 (\x acc -> if x > acc then x else acc)

reverse' :: [a] -> [a]

reverse' = foldl (\acc x -> x : acc) []

product' :: (Num a) => [a] -> a

product' = foldr1 (*)

filter' :: (a -> Bool) -> [a] -> [a]

filter' p = foldr (\x acc -> if p x then x : acc else acc) []

head' :: [a] -> a

head' = foldr1 (\x _ -> x)

last' :: [a] -> a

last' = foldl1 (\_ x -> x)

head est tout de même mieux implémenté par filtrage par motif, mais c’était juste pour l’exemple, que l’on peut y arriver avec des plis. Notre fonction reverse' est à ce titre plutôt astucieuse. On prend pour valeur initiale une liste vide, et on attaque la liste par la gauche en positionnant les éléments rencontrés à l’avant de notre accumulateur. Au final, on a construit la liste renversée. \acc x -> x : acc ressemble assez à la fonction :, mais avec ses paramètres dans l’autre sens. C’est pourquoi, on aurait pu écrire reverse comme foldl (flip (:)) [].

Une autre façon de se représenter les plis à droite et à gauche consiste à se dire : mettons qu’on a un pli à droite et une fonction binaire f, et une valeur initiale z . Si l’on plie à droite la liste [3, 4, 5, 6] , on fait en gros cela : f 3 (f 4 (f 5 (f 6 z))) . f est appelé avec le dernier élément de la liste et

l’accumulateur, cette valeur est donnée comme accumulateur à la prochaine valeur, etc. Si on prend pour f la fonction + et pour accumulateur de départ 0, ça donne 3 + (4 + (5 + (6 + 0))). Ou, avec un + préfixe, (+) 3 ((+) 4 ((+) 5 ((+) 6 0))). De façon similaire, un pli à gauche avec la fonction binaire

g et l’accumulateur z est équivalent p g (g (g (g z 3) 4) 5) 6 . Si on utilise flip (:) comme fonction binaire, et [] comme accumulateur (de manière à renverser la liste), c’est équivalent à flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6. Et pour sûr, évaluer cette expression renvoie

[6, 5, 4, 3] .

scanl et scanr sont comme foldl et foldr , mais rapportent tous les résultats intermédiaires de l’accumulateur sous forme d’une liste. Il existe aussi scanl1 et scanr1, analogues à foldl1 et foldr1.

ghci> scanl (+) 0 [3,5,2,1] [0,3,8,10,11]

ghci> scanr (+) 0 [3,5,2,1] [11,8,3,1,0]

ghci> scanl1 (\acc x -> if x > acc then x else acc) [3,4,5,3,7,9,2,1] [3,4,5,5,7,9,9,9]

ghci> scanl (flip (:)) [] [3,2,1] [[],[3],[2,3],[1,2,3]]

Les scans sont utilisés pour surveiller la progression d’une fonction implémentable comme un pli. Répondons à cette question : Combien d’entiers naturels croissants faut-il pour que la somme de leurs racines carrées dépasse 1000 ? Pour récupérer les racines carrées de tous les entiers naturels, on fait

map sqrt [1..] . Maintenant, pour obtenir leur somme, on pourrait faire un pli, mais puisqu’on s’intéresse à la progression de la somme, on va plutôt faire un scan. Une fois le scan fait, on compte juste le nombre de sommes qui sont inférieures à 1000. La première somme sera normalement égale à 1. La deuxième, à 1 plus racine de 2. La troisième à cela plus racine de 3. Si X de ces sommes sont inférieures à 1000, alors il faut X+1 éléments pour dépasser 1000.

sqrtSums :: Int

sqrtSums = length (takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))) + 1

ghci> sqrtSums 131

ghci> sum (map sqrt [1..131]) 1005.0942035344083

ghci> sum (map sqrt [1..130]) 993.6486803921487

On utilise takeWhile plutôt que filter parce que filter ne peut pas travailler sur des listes infinies. Bien que nous sachions que cette liste est croissante, filter ne le sait pas, donc on utilise takeWhile pour arrêter de scanner dès qu’une somme est plus grande que 1000.