• Aucun résultat trouvé

Utiliser des monoïdes pour plier des structures de données

Une des façons les plus intéressantes de mettre les monoïdes à l’usage est de les utiliser pour nous aider à définir des plis sur diverses structures de données. Jusqu’ici, nous n’avons fait que des plis sur des listes, mais les listes ne sont pas les seules structures de données pliables. On peut définir des plis sur presque toute structure de données. Les arbres se prêtent particulièrement bien à l’exercice du pli.

Puisqu’il y a tellement de structures de données qui fonctionnent bien avec les plis, la classe de types Foldable a été introduite. Comme Functor est pour les choses sur lequelles on peut mapper, Foldable est pour les choses qui peuvent être pliées ! Elle est trouvable dans Data.Foldable et puisqu’elle exporte des fonctions dont les noms sont en collision avec ceux du Prelude, elle est préférablement importée qualifiée (et servie avec du basilic) :

import qualified Foldable as F

Pour nous éviter de précieuses frappes de clavier, on l’importe qualifiée par F. Bien, donc quelles sont les fonctions que cette classe de types définit ? Eh bien, parmi celles-ci sont foldr, foldl, foldr1, foldl1. Hein ? Mais on connaît déjà ces fonctions, quoi de neuf ? Comparons le type de foldr dans Foldable avec celui de foldr du Prelude pour voir ce qui diffère :

ghci> :t foldr

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

ghci> :t F.foldr

F.foldr :: (F.Foldable t) => (a -> b -> b) -> b -> t a -> b

Ah ! Donc, alors que foldr prend une liste et la plie, le foldr de Data.Foldable accepte tout type qui peut être plié, pas seulement les listes ! Comme on peut s’y attendre, les deux fonctions font la même chose sur les listes :

ghci> foldr (*) 1 [1,2,3] 6

ghci> F.foldr (*) 1 [1,2,3] 6

Ok, quelles autres structures peuvent être pliées ? Eh bien, le Maybe qu’on connaît tous et qu’on aime tant !

ghci> F.foldl (+) 2 (Just 9) 11

ghci> F.foldr (||) False (Just True) True

Mais plier une simple valeur Maybe n’est pas très intéressant, parce que quand il s’agit de se plier, elle se comporte comme une liste avec un élément si c’est un Just , et comme une liste vide si c’est Nothing . Examinons donc une structure de données un peu plus complexe.

Vous rappelez-vous la structure de données arborescente du chapitre Créer nos propres types et classes de types ? On l’avait définie ainsi :

data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

On disait qu’un arbre était soit un arbre vide qui ne contient aucune valeur, soit un neud qui contient une valeur ainsi que deux autres arbres. Après l’avoir défini, on en a fait une instance de Functor et on a gagné la possibilité de faire fmap sur ce type. À présent, on va en faire une instance de Foldable afin de pouvoir le plier. Une façon de faire d’un constructeur de types une instance de Foldable consiste à implémenter directement foldr. Une autre façon, souvent bien plus simple, consiste à implémenter la fonction foldMap, qui fait aussi partie de la classe de types Foldable. foldMap a pour type :

foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Son premier paramètre est une fonction qui prend une valeur du type que notre structure de données pliable contient (ici dénoté a) et retourne une valeur monoïdale. Son second paramètre est une structure pliable contenant des valeurs de type a. Elle mappe la fonction sur la structure pliable, produisant ainsi une structure pliable contenant des valeurs monoïdales. Ensuite, en faisant mappend entre toutes ces valeurs monoïdales, elle les joint en une unique valeur

monoïdale. Cette fonction peut paraître bizarre pour l’instant, mais on va voir qu’elle est très simple à implémenter. Ce qui est aussi cool, c’est qu’il suffit

simplement d’implémenter cette fonction pour que notre type soit une instance de Foldable. Ainsi, si l’on implémente foldMap pour un type, on obtient foldr et foldl sans effort !

Voici comment un Tree est fait instance de Foldable :

instance F.Foldable Tree where

foldMap f Empty = mempty

foldMap f (Node x l r) = F.foldMap f l `mappend` f x `mappend` F.foldMap f r

On pense ainsi : si l’on nous donne une fonction qui prend un élément de notre arbre et retourne une valeur monoïdale, comment réduit-on notre arbre à une simple valeur monoïdale ? Quand nous faisions fmap sur notre arbre, on appliquait la fonction mappée au nœud, puis on mappait

récursivement la fonction sur le sous-arbre gauche et sur le sous-arbre droit. Ici, on nous demande non seulement de mapper la fonction, mais également de joindre les résultats en une simple valeur monoïdale à l’aide de mappend. On considère d’abord le cas de l’arbre vide - un pauvre arbre tout triste et solitaire, sans valeur ni sous-arbre. Il n’a pas de valeur qu’on puisse passer à notre fonction créant des monoïdes, donc si notre arbre est vide, la valeur monoïdale sera mempty.

Le cas d’un nœud non vide est un peu plus intéressant. Il contient deux sous-arbres ainsi qu’une valeur. Dans ce cas, on foldMap récursivement la même fonction f sur les sous-arbres gauche et droit. Souvenez-vous, notre foldMap retourne une simple valeur monoïdale. On applique également la fonction f à la valeur du nœud. À présent, nous avons trois valeurs monoïdales (deux venant des

sous-arbres et une venant de l’application de f sur la valeur du nœud) et il suffit de les écraser en une seule valeur. Pour ce faire, on utilise mappend, et naturellement, le sous-arbre gauche vient avant la valeur du nœud, suivi du sous-arbre droit.

Remarquez qu’on n’a pas eu besoin d’écrire la fonction qui prend une valeur et retourne une valeur monoïdale. On la reçoit en paramètre de la fonction foldMap et tout ce qu’on a besoin de décider est où l’appliquer et comment joindre les monoïdes qui en résultent.

Maintenant qu’on a une instance de Foldable pour notre type arborescent, on a foldr et foldl gratuitement ! Considérez cet arbre :

testTree = Node 5 (Node 3

(Node 1 Empty Empty) (Node 6 Empty Empty) )

(Node 9

(Node 8 Empty Empty) (Node 10 Empty Empty) )

Il a 5 pour racine, puis son nœud gauche contient 3 avec 1 à sa gauche et 6 à sa droite. Le nœud droit de la racine contient 9 avec un 8 à sa gauche et un 10 tout à droite. Avec une instance de Foldable , on peut faire tous les plis qu’on fait sur des listes :

ghci> F.foldl (+) 0 testTree 42

ghci> F.foldl (*) 1 testTree 64800

foldMap ne sert pas qu’à créer les instances de Foldable , elle sert également à réduire une structure à une simple valeur monoïdale. Par exemple, si l’on voulait savoir si n’importe quel nombre de notre arbre est égal à 3, on pourrait faire :

ghci> getAny $ F.foldMap (\x -> Any $ x == 3) testTree True

Ici, \x -> Any $ x == 3 est une fonction qui prend un nombre et retourne une valeur monoïdale, dans ce cas un Bool enveloppé en Any. foldMap applique la fonction à chaque élément de l’arbre, puis réduit les monoïdes résultants en une unique valeur monoïdale à l’aide de mappend. Si l’on faisait :

ghci> getAny $ F.foldMap (\x -> Any $ x > 15) testTree False

Tous les nœuds de notre arbre contiendraient la valeur Any False après avoir appliqué la fonction dans la lambda à chacun d’eux. Pour valoir True, mappend sur Any doit avoir au moins un True passé en paramètre. C’est pourquoi le résultat final est False, ce qui est logique puisqu’aucune valeur de notre arbre n’excède 15.

On peut aussi facilement transformer notre arbre en une liste en faisant foldMap avec la fonction \x -> []. En projetant d’abord la fonction sur l’arbre, chaque élément devient une liste singleton. Puis, mappend a lieu entre tous ces singletons et retourne une unique liste qui contient tous les éléments de notre arbre :

ghci> F.foldMap (\x -> [x]) testTree [1,3,6,5,8,9,10]

Ce qui est vraiment cool, c’est que toutes ces techniques ne sont pas limitées aux arbres, elles fonctionnent pour toute instance de Foldable.