• 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 chosex:xs 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 mapmap , seulement qu’elles réduisent une liste à une valeur unique.

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

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\acc x -> acc + x est la fonction binaire. 00 est la valeur de départ et xs la liste à plier. D’abord, 0xs 0 est utilisé pour accacc et 33 pour xx . 0 + 30 + 3 retourne 33 et devient, pour ainsi

dire, le nouvel accumulateur. Ensuite, 33 est utilisé comme accumulateur, et l’élément courant, 55 , résultant en un 88 comme nouvel accumulateur. Encore en avant, 88 est l’accumulateur, 22 la valeur courante, le nouvel accumulateur est donc 1010 . Utilisé avec la valeur courante 11 , il produit 1111 . 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 à (+)(\acc x -> acc + x) (+) . On peut omettre le xsxs à la fin parce que foldl (+) 0

foldl (+) 0 retourne une fonction qui attend une liste. En général, lorsque vous avec une fonction foo a = bar b a

foo a = bar b a , vous pouvez réécrire foo = bar bfoo = 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’elemelem 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 FalseFalse . Il est logique d’utiliser FalseFalse 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 premierfold é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’ilTrue était FalseFalse , il reste à FalseFalse car on ne vient pas de trouver l’élément. S’il était TrueTrue , on le laisse aussi.

Le pli à droite, foldrfoldr 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\acc x -> … la valeur courante en premier et l’accumulateur en second (\x acc -> …\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, ilmap 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)(+3) à [1, 2, 3][1, 2, 3] , on attaque la liste par la droite. On prend le dernier élément, 33 et on applique la fonction, résultant en un 66 . On le place à l’avant de l’accumulateur, qui était [] . 6:[][] 6:[] est [6][6] , et notre nouvel accumulateur. On applique (+3)(+3) sur 22 , donnant 55 et on le place devant (: ) l’accumulateur, l’accumulateur devient [5, 6]: [5, 6] . On applique (+3)(+3) à 11 et on le place devant l’accumulateur, ce qui donne pour valeur finale [4, 5, 6][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

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 sumsum 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 foldl1foldl1 et foldr1foldr1 fonctionnent quasiment comme foldlfoldl et foldrfoldr , 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 (+)sum 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 etfoldl

foldr

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

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 positionnantreverse' les éléments rencontrés à l’avant de notre accumulateur. Au final, on a construit la liste renversée. \acc x -> x : acc\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 (:)) []: 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 unef valeur initiale zz . Si l’on plie à droite la liste [3, 4, 5, 6][3, 4, 5, 6] , on fait en gros cela : f 3 (f 4 (f 5 (f 6 z)))f 3 (f 4 (f 5 (f 6 z))) . ff 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 +f + et pour accumulateur de départ 0 , ça donne 3 + (4 + (5 + (6 + 0)))0 3 + (4 + (5 + (6 + 0))) . Ou, avec un ++ préfixe, (+) 3 ((+) 4 ((+) 5 ((+) 6 0)))(+) 3 ((+) 4 ((+) 5 ((+) 6 0))) . De façon similaire, un pli à gauche avec la fonction binaire g et l’accumulateur zg z est équivalent p g (g (g (g z 3) 4) 5) 6g (g (g (g z 3) 4) 5) 6 . Si on utilise

flip (:)

flip (:) comme fonction binaire, et [][] comme accumulateur (de manière à renverser la liste), c’est équivalent à flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6

flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6 . Et pour sûr, évaluer cette expression renvoie [6, 5, 4, 3][6, 5, 4, 3] . scanl

scanl et scanrscanr sont comme foldlfoldl et foldrfoldr , mais rapportent tous les résultats intermédiaires de l’accumulateur sous forme d’une liste. Il existe aussi scanl1 et scanr1scanl1 scanr1 , analogues à foldl1foldl1 et foldr1foldr1 .

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

En utilisant un scanl , le résultat final sera dans le dernier élément de la liste retournée, alors que pour un scanrscanl scanr , il sera en tête.

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

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 filtertakeWhile filter parce que filterfilter ne peut pas travailler sur des listes infinies. Bien que nous sachions que cette liste est croissante, filterfilter ne le sait pas, donc on utilise takeWhiletakeWhile pour arrêter de scanner dès qu’une somme est plus grande que 1000.

Appliquer des fonctions avec $

Bien, maintenant, découvrons la fonction $ , aussi appelée application de fonction. Voyons sa définition :$ ($) :: (a -> b) -> a -> b

f $ x = f x

De quoi ? Qu’est-ce que c’est que cet opérateur inutile ? C’est juste une application de fonction ! Eh bien, presque, mais pas complètement ! Alors que l’application de fonction habituelle (avec une espace entre deux choses) a une précédence très élevée, la fonction $$ a la plus faible précédence. Une application de fonction avec une espace est associative à gauche (f a b c est équivalent à ((f a) b) cf a b c ((f a) b) c ), alors qu’avec $$ elle est associative à droite. C’est tout, mais en quoi cela nous aide-t-il ? La plupart du temps, c’est une fonction pratique pour éviter d’écrire des tas de parenthèses. Considérez l’expression sum (map sqrt [1..130])sum (map sqrt [1..130]) . Puisque $$ a une précédence aussi faible, on peut réécrire cette expression sum $ map sqrt [1..130] , et éviter de précieuses frappes de clavier ! Quand on rencontre un $sum $ map sqrt [1..130] $ , la fonction sur sa gauche s’applique à l’expression sur sa droite. Qu’en est-il de sqrt 3 + 4 + 9 ? Ceci ajoute 9, 4, et la racine de 3. Mais si l’onsqrt 3 + 4 + 9 veut la racine carrée de 3 + 4 + 9, il faut écrire sqrt (3 + 4 + 9) , ou avec $sqrt (3 + 4 + 9) $ , sqrt $ 3 + 4 + 9sqrt $ 3 + 4 + 9 , car $$ a la plus faible précédence de tous les opérateurs. On peut donc voir $ comme le fait d’écrire une parenthèse ouvrante en lieu et place, et d’aller mettre une parenthèse fermante le$ plus possible à droite de l’expression.

Et sum (filter (> 10) (map (*2) [2..10])) ? Et bien, puisque $sum (filter (> 10) (map (*2) [2..10])) $ est associatif à droite, f (g (z x))f (g (z x)) est égal à f $ g $ z xf $ g $ z x . On peut donc réécrire l’expression sum $ filter (> 10) $ map (*2) [2..10]sum $ filter (> 10) $ map (*2) [2..10] .

À part pour se débarasser des parenthèses, $ implique aussi que l’on peut traiter l’application de fonction comme n’importe quelle fonction. Ainsi,$ on peut par exemple mapper l’application de fonction sur une liste de fonctions.

ghci> map ($ 3) [(4+), (10*), (^2), sqrt] [7.0,30.0,9.0,1.7320508075688772]