• Aucun résultat trouvé

Dans cette section, on va regarder comment un type est créé, identifié comme une monade, et instancié en Monad. Généralement, on ne crée pas une monade pour le plaisir de créer une monade. Généralement, on crée plutôt un type dont le but est de modéliser un aspect du problème à résoudre, et plus tard si l’on s’aperçoit que ce type représente une valeur dans un contexte et peut agir comme une monade, on lui donne une instance de Monad.

Comme on l’a vu, les listes sont utilisées pour représenter des valeurs non déterministes. Une liste comme [3, 5, 9] peut être vue comme une unique valeur non déterministe qui ne peut tout simplement pas décider de ce qu’elle veut être. Quand on donne une liste à une fonction avec >>=, cela fait juste tous les choix possibles pour appliquer la fonction sur un élément de la liste, et le résultat est une liste présentant tous ces résultats.

Si l’on regarde la liste [3, 5, 9] comme les nombres 3, 5 et 9 à la fois, on peut remarquer qu’il n’y a pas d’information quand à la probabilité de chacun d’eux. Et si l’on voulait modéliser une valeur non déterministe comme [3, 5, 9], mais qu’on souhaitait exprimer que 3 a 50% de chances d’avoir lieu, alors que 5 et 9 n’ont chacun que 25% de chances ? Essayons d’y arriver !

Mettons que chaque élément de la liste vienne avec une valeur supplémentaire, une probabilité. Il peut sembler logique de le présenter ainsi : [(3,0.5),(5,0.25),(9,0.25)]

En mathématiques, on n’utilise généralement pas des pourcentages pour exprimer les probabilités, mais plutôt des nombres réels entre 0 et 1. Un 0 signifie que quelque chose n’a aucune chance au monde d’avoir lieu, et un 1 signifie qu’elle aura lieu à coup sûr. Les nombres à virgule flottante peuvent rapidement devenir bordéliques parce qu’ils ont tendance à perdre en précision, ainsi Haskell propose un type de données pour les nombres rationnels qui ne perdent pas en précision. Ce type s’appelle Rational et vit dans Data.Ratio. Pour créer un Rational, on l’écrit comme si c’était une fraction. Le numérateur et le dénominateur sont séparés par un %. Voici quelques exemples :

ghci> 1%4 1 % 4 ghci> 1%2 + 1%2 1 % 1 ghci> 1%3 + 5%4 19 % 12

La première ligne est juste un quart. À la deuxième ligne, on additionne deux moitiés, ce qui nous donne un tout, et à la troisième ligne on additionne un tiers et cinq quarts et on obtient dix-neuf douzièmes. Jetons ces nombres à virgule flottante et utilisons plutôt des Rational pour nos probabilités :

ghci> [(3,1%2),(5,1%4),(9,1%4)] [(3,1 % 2),(5,1 % 4),(9,1 % 4)]

Ok, donc 3 a une chance sur deux d’avoir lieu, alors que 5 et 9 arrivent une fois sur quatre. Plutôt propre.

Nous avons pris des listes, et ajouter un contexte supplémentaire, afin qu’elles représentent des valeurs avec des contextes. Avant d’aller plus loin, enveloppons cela dans un newtype parce que mon petit doigt me dit qu’on va bientôt créer des instances.

import Data.Ratio

newtype Prob a = Prob { getProb :: [(a,Rational)] } deriving Show

Bien. Est-ce un foncteur ? Eh bien, la liste étant un foncteur, ceci devrait probablement être également un foncteur, parce qu’on a juste rajouté quelque chose à la liste. Lorsqu’on mappe une fonction sur une liste, on l’applique à chaque élément. Ici, on va l’appliquer à chaque élément également, et on laissera les probabilités comme elles étaient. Créons une instance :

instance Functor Prob where

fmap f (Prob xs) = Prob $ map (\(x,p) -> (f x,p)) xs

On sort la paire du newtype en filtrant par motif, on applique la fonction f aux valeurs en gardant les probabilités telles quelles, et on réencapsule le tout. Voyons si cela marche :

ghci> fmap negate (Prob [(3,1%2),(5,1%4),(9,1%4)]) Prob {getProb = [(-3,1 % 2),(-5,1 % 4),(-9,1 % 4)]}

Autre chose à noter, les probabilités devraient toujours avoir pour somme 1. Si ce sont bien toutes les choses qui peuvent avoir lieu, il serait illogique que la somme de leur probabilité ne fasse pas 1. Une pièce qui a 75% de chances de faire pile et 50% de chances de faire face ne semble pouvoir marcher que dans un autre étrange univers.

Maintenant, la grande question, est-ce une monade ? Étant donné que la liste est une monade, on dirait que ça devrait aussi être une monade. Pensons d’abord à return. Comment marche-t-elle sur les listes ? Elle prend une valeur, et la place dans une liste singleton. Qu’en est-il ici ? Eh bien, puisque c’est censé être un contexte minimal par défaut, cela devrait aussi être une liste singleton. Qu’en est-il de la probabilité ? Eh bien, return x est supposée créer une valeur

monadique qui présente toujours x en résultat, il serait donc illogique que la probabilité soit 0. Si elle doit toujours la présenter en résultat, la probabilité devrait être 1 !

Qu’en est-il de >>= ? Ça semble un peu compliqué, profitons donc du fait que m >>= f est toujours égal à join (fmap f m) pour les monades, et pensons plutôt à la façon dont on aplatirait une liste de probabilités de listes de probabilités. Comme exemple, considérons une liste où il y a exactement 25% de chances que 'a' ou 'b' ait lieu. a et b ont la même probabilité. Également, il y a 75% de chances que c ou d ait lieu. 'c' et 'd' ont également la même probabilité. Voici une image de la liste de probabilités qui modélise ce scénario :

Quelles sont les chances que chacune de ces lettres ait lieu ? Si l’on devait redessiner ceci avec quatre boîtes, chacune avec une probabilité, quelles seraient ces

probabilités ? Pour les trouver, il suffit de multiplier chaque probabilité par la

probabilité qu’elle contient. 'a' aurait lieu une fois sur huit, et de même pour 'b', parce que si l’on multiplie un demi par un quart, on obtient un huitième. 'c' aurait lieu trois fois sur huit parce que trois quarts multipliés par un demi donnent trois huitièmes. 'd' aurait aussi lieu trois fois sur huit. Si l’on somme toutes les probabilités, la somme vaut toujours un.

Voici cette situation exprimée comme une liste de probabilités :

thisSituation :: Prob (Prob Char)

thisSituation = Prob

[( Prob [('a',1%2),('b',1%2)] , 1%4 ) ,( Prob [('c',1%2),('d',1%2)] , 3%4) ]

Remarquez que son type est Prob (Prob Char). Maintenant qu’on a trouvé comment aplatir une liste de probabilités imbriquée, il nous suffit d’écrire le code correspondant, et on peut alors écrire >>= comme join (fmap f m) et avoir notre monade ! Voici flatten, qu’on nomme ainsi parce que le nom join est déjà pris :

flatten :: Prob (Prob a) -> Prob a

flatten (Prob xs) = Prob $ concat $ map multAll xs

where multAll (Prob innerxs,p) = map (\(x,r) -> (x,p*r)) innerxs

La fonction multAll prend un tuple formé d’une liste de probabilités et d’une probabilité p et multiplie toutes les probabilités à l’intérieur de cette première par p, retournant une liste de paires d’éléments et de probabilités. On mappe multAll sur chaque paire de notre liste de probabilités imbriquée et on aplatit simplement la liste imbriquée résultante.

On a à présent tout ce dont on a besoin pour écrire une instance de Monad !

instance Monad Prob where

return x = Prob [(x,1%1)] m >>= f = flatten (fmap f m) fail _ = Prob []

Puisqu’on a déjà fait tout le travail, l’instance est très simple. On a aussi défini la fonction fail, qui est la même que pour les listes, donc en cas d’échec d’un filtrage par motif dans une expression do, un échec a lieu dans le contexte d’une liste de

probabilités.

Il est également important de vérifier si les lois des monades tiennent pour l’instance qu’on vient de créer. La première dit que return x >>= f devrait être égal à f x . Une preuve rigoureuse serait fastidieuse, mais on peut voir que si l’on place une valeur dans un contexte par défaut avec return et qu’on fmap une fonction par dessus, puis qu’on aplatit la liste de probabilités

résultante, chaque probabilité résultant de la fonction serait multipliée par la probabilité 1%1 créée par return, donc le contexte serait inaffecté. Le raisonnement montrant que m >>= return est égal à m est similaire. La troisième loi dit que

f <=< (g <=< h) devrait être égal à (f <=< g) <=< h . Puisque cette loi tient pour la monade des listes, qui est la base de la monade des probabilités, et parce que la multiplication est associative, cette loi tient donc également pour la monade des

probabilités. 1%2 * (1%3 * 1%5) est égal à (1%2 * 1%3) * 1%5.

À présent qu’on a une monade, que peut-on en faire ? Eh bien, elle peut nous aider à faire des calculs avec des probabilités. On peut traiter des évènements probabilistes comme des valeurs dans des contextes, et la monade de probabilité s’assurera que les probabilités sont bien reflétées dans le résultat final.

Mettons qu’on ait deux pièces normales et une pièce pipée qui donne pile un nombre ahurissant de neuf fois sur dix, et face seulement une fois sur dix. Si l’on jette les trois pièces à la fois, quelles sont les chances qu’elles atterrissent toutes sur pile ? D’abord, créons des valeurs de probabilités pour un lancer de pièce normale et un lancer de pièce pipée :

data Coin = Heads | Tails deriving (Show, Eq)

coin :: Prob Coin

coin = Prob [(Heads,1%2),(Tails,1%2)]

loadedCoin :: Prob Coin

loadedCoin = Prob [(Heads,1%10),(Tails,9%10)]

Et finalement, le lancer de pièces en action :

import Data.List (all)

flipThree :: Prob Bool

flipThree = do

a <- coin b <- coin

c <- loadedCoin

return (all (==Tails) [a,b,c])

En l’essayant, on voit que les chances que les trois pièces atterrissent sur pile ne sont pas très bonnes, en dépit d’avoir triché avec notre pièce pipée :

ghci> getProb flipThree

[(False,1 % 40),(False,9 % 40),(False,1 % 40),(False,9 % 40), (False,1 % 40),(False,9 % 40),(False,1 % 40),(True,9 % 40)]

Toutes les trois atterriront sur pile neuf fois sur quarante, ce qui fait moins de 25% de chances. On voit que notre monade ne sait pas joindre tous les résultats False où les pièces ne tombent pas toutes sur pile en un seul résultat. Ce n’est pas un gros problème, puisqu’écrire une fonction qui regroupe tous ses résultats en un seul est plutôt facile et est laissé comme exercice pour le lecteur (vous !).

Dans cette section, on est parti d’une question (et si les listes transportaient également une information de probabilité ?) pour créer un type, reconnaître une monade et finalement créer une instance et faire quelque chose avec elle. Je trouve ça plutôt attrayant ! À ce stade, on devrait avoir une assez bonne compréhension de ce que sont les monades.

Zippeurs

Et pour quelques monades de plus Table des matières

Alors que la pureté d’Haskell amène tout un tas de bienfaits, elle nous oblige à attaquer certains problèmes sous un autre angle que celui qu’on aurait pris dans des langages impurs. À cause de la transparence référentielle, une valeur est aussi bonne qu’une autre en Haskell si elles représentent toutes les deux la même chose.

Donc si on a un arbre plein de cinq (tapez-m’en cinq ?) et qu’on veut en changer un en un six, il nous faut un moyen de savoir exactement quel cinq dans notre arbre on veut changer. Il faut savoir où il est dans l’arbre. Dans les langages impurs, on aurait pu noter l’adresse mémoire où est situé le cinq, et changer la valeur à cette adresse. Mais en Haskell, un cinq est un cinq comme les autres, et on ne peut donc pas le discriminer en fonction de sa position en mémoire. On ne peut pas non plus vraiment changer quoi que ce soit, et quand on dit qu’on change un arbre, on veut en fait dire qu’on prend un arbre et qu’on en retourne un autre, similaire à l’original mais un peu différent. Une possibilité consiste à se rappeler du chemin de la racine de l’arbre à l’élément qu’on souhaite changer. On pourrait dire : prends cet arbre, va à gauche, va à droite, et encore à gauche, et change l’élément qui est à cet endroit. Bien que cela marche, ça peut être

inefficace. Si l’on veut plus tard changer un élément qui est juste à côté de celui qu’on vient de changer, on doit traverser l’arbre à nouveau depuis la racine jusqu’à cet élément !

Dans ce chapitre, nous allons voir qu’on peut prendre une structure de données et se focaliser sur une partie de celle-ci de façon à ce que modifier ses éléments soit facile, et à ce que se balader autour soit efficace. Joli !