• Aucun résultat trouvé

Nous avons chargé notre pistolet avec les monades MaybeMaybe , liste et IOIO . Plaçons maintenant la monade WriterWriter dans la chambre, et voyons ce qui se passe quand on tire !

Alors que MaybeMaybe est pour les valeurs ayant un contexte additionnel d’échec, et que les listes sont pour les valeurs non déterministes, la monade Writer

Writer est faite pour les valeurs qui peuvent avoir une autre valeur attachée agissant comme une sorte de registre. WriterWriter nous permet d’effectuer des calculs tout en étant sûrs que toutes les valeurs du registre sont bien combinées en un registre qui reste attaché au résultat. Par exemple, on peut vouloir munir nos valeurs d’une chaîne de caractères décrivant ce qui se passe, probablement pour déboguer notre programme. Considérez une fonction qui prend un nombre de bandits d’un gang et nous dit si c’est un gros gang ou pas. C’est une fonction très simple :

isBigGang :: Int -> Bool

isBigGang x = x > 9

Maintenant, et si au lieu de nous répondre seulement True ou FalseTrue False , on voulait aussi retourner une chaîne de caractères indiquant ce qu’on a fait ? Eh bien, il suffit de créer une chaîne et la retourner avec le BoolBool :

isBigGang :: Int -> (Bool, String)

isBigGang x = (x > 9, "Compared gang size to 9.")

À présent, au lieu de retourner juste un Bool , on retourne un tuple dont la première composante est la vraie valeur de retour, et la seconde est laBool chaîne accompagnant la valeur. Il y a un contexte additionnel à présent. Testons :

ghci> isBigGang 3

(False,"Compared gang size to 9.")

ghci> isBigGang 30

Jusqu’ici, tout va bien. isBigGangisBigGang prend une valeur normale et retourne une valeur dans un contexte. Comme on vient de le voir, lui donner une valeur normale n’est pas un problème. Et si l’on avait déjà une valeur avec un registre attaché, comme (3, "Smallish gang.") , et qu’on voulait la donner à isBigGang(3, "Smallish gang.") isBigGang ? Il semblerait qu’on se retrouve à nouveau face à la question : si l’on a une fonction qui prend une valeur normale et retourne une valeur dans un contexte, comment lui passer une valeur dans un contexte ?

Quand on explorait la monade MaybeMaybe , on a créé une fonction applyMaybeapplyMaybe , qui prenait un Maybe aMaybe a et une fonction de type a -> Maybe ba -> Maybe b et on donnait la valeur Maybe aMaybe a à la fonction, bien qu’elle attende un aa normal et pas un Maybe a . Ceci était fait en prenant en compte le contexte qui venait avec la valeur Maybe aMaybe a Maybe a , qui était celui de l’échec potentiel. Mais une fois dans la fonction a -> Maybe ba -> Maybe b , on pouvait traiter la valeur comme une valeur normale, parce que applyMaybeapplyMaybe (qui devint ensuite >>=>>= ) s’occupait de vérifier si c’était un

Nothing

Nothing ou un JustJust .

Dans la même veine, créons une fonction qui prend une valeur avec un registre attaché, c’est-à-dire, de type (a, String)(a, String) , et une fonction a -> (b, String)

a -> (b, String) , et qui donne cette valeur à cette fonction. On va l’appeler applyLogapplyLog . Mais puisqu’une valeur (a, String)(a, String) ne contient pas le contexte d’échec potentiel, mais plutôt un contexte de valeur additionnelle, applyLogapplyLog va s’assurer que le registre de la valeur originale n’est pas perdu, mais est accolé au registre de la valeur résultant de la fonction. Voici l’implémentation d’applyLogapplyLog :

applyLog :: (a,String) -> (a -> (b,String)) -> (b,String)

applyLog (x,log) f = let (y,newLog) = f x in (y,log ++ newLog)

Quand on a une valeur dans un contexte et qu’on souhaite la donner à une fonction, on essaie généralement de séparer la vraie valeur du contexte, puis on applique la fonction à cette valeur, et on s’occupe enfin de la gestion du contexte. Dans la monade Maybe , on vérifiait si la valeurMaybe était un Just xJust x et si c’était le cas, on prenait ce xx et on appliquait la fonction. Dans ce cas, il est encore plus simple de trouver la vraie valeur, parce qu’on a une paire contenant la valeur et un registre. On prend simplement la première composante, qui est x et on applique fx f avec. On obtient une paire (y, newLog)(y, newLog) , où yy est le nouveau résultat, et newLognewLog le nouveau registre. Mais si l’on retournait cela en résultat, on aurait oublié l’ancien registre, ainsi on retourne une paire (y, log ++ newLog) . On utilise ++(y, log ++ newLog) ++ pour juxtaposer le nouveau registre et l’ancien. Voici applyLog en action :applyLog

ghci> (3, "Smallish gang.") `applyLog` isBigGang (False,"Smallish gang.Compared gang size to 9")

ghci> (30, "A freaking platoon.") `applyLog` isBigGang (True,"A freaking platoon.Compared gang size to 9")

Les résultats sont similaires aux précédents, seulement le nombre de personne dans le gang avait un registre l’accompagnant, et ce registre a été inclus dans le registre résultant. Voici d’autres exemples d’utilisation d’applyLogapplyLog :

ghci> ("Tobin","Got outlaw name.") `applyLog` (\x -> (length x, "Applied length.")) (5,"Got outlaw name.Applied length.")

ghci> ("Bathcat","Got outlaw name.") `applyLog` (\x -> (length x, "Applied length")) (7,"Got outlaw name.Applied length")

Voyez comme, dans la lambda, xx est simplement une chaîne de caractères normale et non pas un tuple, et comment applyLogapplyLog s’occupe de la juxtaposition des registres.

Monoïdes à la rescousse

Soyez certain de savoir ce que sont les monoïdes avant de continuer ! Cordialement.

Pour l’instant, applyLog prend des valeurs de type (a, String)applyLog (a, String) , mais y a-t-il une raison à ce que le registre soit une StringString ? On utilise ++

++ pour juxtaposer les registres, ne devrait-ce donc pas marcher pour n’importe quel type de liste, pas seulement des listes de caractères ? Bien sûr que oui. On peut commencer par changer son type en :

À présent, le registre est une liste. Le type des valeurs dans la liste doit être le même dans la valeur originale que dans la valeur retournée par la fonction, autrement on ne saurait utiliser ++++ pour les juxtaposer.

Est-ce que cela marcherait pour des chaînes d’octets ? Il n’y a pas de raison que ça ne marche pas. Cependant, le type qu’on a là ne marche que pour les listes. On dirait qu’il nous faut une autre fonction applyLog pour les chaînes d’octets. Mais attendez ! Les listes et les chaînes d’octetsapplyLog sont des monoïdes. En tant que tels, elles sont toutes deux des instances de la classe de types MonoidMonoid , ce qui signifie qu’elles implémentent la fonction mappend . Et pour les listes autant que les chaînes d’octets, mappendmappend mappend sert à concaténer. Regardez :

ghci> [1,2,3] `mappend` [4,5,6] [1,2,3,4,5,6]

ghci> B.pack [99,104,105] `mappend` B.pack [104,117,97,104,117,97] Chunk "chi" (Chunk "huahua" Empty)

Cool ! Maintenant, applyLog peut fonctionner sur n’importe quel monoïde. On doit changer son type pour refléter cela, ainsi que sonapplyLog implémentation pour remplacer ++++ par mappendmappend :

applyLog :: (Monoid m) => (a,m) -> (a -> (b,m)) -> (b,m)

applyLog (x,log) f = let (y,newLog) = f x in (y,log `mappend` newLog)

Puisque la valeur accompagnante peut être n’importe quelle valeur monoïdale, plus besoin de penser à un tuple valeur et registre, on peut désormais penser à un tuple valeur et valeur monoïdale. Par exemple, on peut avoir un tuple contenant un nom d’objet et un prix en tant que valeur monoïdale. On utilise le newtype Sumnewtype Sum pour s’assurer que les prix sont bien additionnés lorsqu’on opère sur les objets. Voici une fonction qui ajoute des boissons à de la nourriture de cow-boy :

import Data.Monoid

type Food = String

type Price = Sum Int

addDrink :: Food -> (Food,Price)

addDrink "beans" = ("milk", Sum 25)

addDrink "jerky" = ("whiskey", Sum 99)

addDrink _ = ("beer", Sum 30)

On utilise des chaînes de caractères pour représenter la nourriture, et un Int dans un newtypeInt newtype SumSum pour tracer le nombre de centimes que quelque chose coûte. Juste un rappel, faire mappendmappend sur des SumSum résulte en la somme des valeurs enveloppées :

ghci> Sum 3 `mappend` Sum 9

Sum {getSum = 12}

La fonction addDrinkaddDrink est plutôt simple. Si l’on mange des haricots, elle retourne "milk""milk" ainsi que Sum 25Sum 25 , donc 25 centimes encapsulés dans un SumSum . Si l’on mange du bœuf séché, on boit du whisky, et si l’on mange quoi que ce soit d’autre, on boit une bière. Appliquer normalement une fonction à de la nourriture ne serait pas très intéressant ici, mais utiliser applyLogapplyLog pour donner une nourriture qui a un prix à cette fonction est intéressant :

ghci> ("beans", Sum 10) `applyLog` addDrink ("milk",Sum {getSum = 35})

ghci> ("jerky", Sum 25) `applyLog` addDrink ("whiskey",Sum {getSum = 124})

ghci> ("dogmeat", Sum 5) `applyLog` addDrink ("beer",Sum {getSum = 35})

Du lait coûte 25 centimes, mais si on le prend avec des haricots coûtant 1025 10 centimes, on paie au final 3535 centimes. Il est à présent clair que la valeur attachée n’a pas besoin d’être un registre, elle peut être n’importe quel valeur monoïdale, et la façon dont deux de ces valeurs sont combinées dépend du monoïde. Quand nous faisions des registres, elles étaient juxtaposées, mais à présent, les nombres sont sommés.

Puisque la valeur qu’addDrinkaddDrink retourne est un tuple (Food, Price)(Food, Price) , on peut donner ce résultat à addDrinkaddDrink à nouveau, pour qu’elle nous dise ce qu’on devrait boire avec notre boisson et combien le tout nous coûterait. Essayons :

("beer",Sum {getSum = 65})

Ajouter une boisson à de la nourriture pour chien retourne une bière et un prix additionnel de 30 centimes, donc ("beer", Sum 35)30 ("beer", Sum 35) . Et si l’on utilise applyLog pour donner cela à addDrinkapplyLog addDrink , on obtient une autre bière et le résultat est ("beer", Sum 65)("beer", Sum 65) .

Le type Writer

Maintenant qu’on a vu qu’une valeur couplée à un monoïde agissait comme une valeur monadique, examinons l’instance de Monad pour de telsMonad types. Le module Control.Monad.Writer exporte le type Writer w aControl.Monad.Writer Writer w a ainsi que son instance de MonadMonad et quelques fonctions utiles pour manipuler des valeurs de ce type.

D’abord, examinons le type lui-même. Pour attacher un monoïde à une valeur, on doit simplement les placer ensemble dans un tuple. Le type Writer w a

Writer w a est juste un enrobage newtypenewtype de cela. Sa définition est très simple : newtype Writer w a = Writer { runWriter :: (a, w) }

C’est enveloppé dans un newtypenewtype afin d’être fait instance de MonadMonad et de séparer ce type des tuples ordinaires. Le paramètre de type aa représente le type de la valeur, alors que le paramètre de type w est la valeur monoïdale attachée.w

Son instance de Monad est définie de la sorte :Monad

instance (Monoid w) => Monad (Writer w) where return x = Writer (x, mempty)

(Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')

Tout d’abord, examinons >>=>>= . Son implémentation est essentiellement identique à applyLog

applyLog , seulement à présent que notre tuple est enveloppé dans un newtype

newtype WriterWriter , on doit l’en sortir en filtrant par motif. On prend la valeur xx et applique la fonction f . Cela nous rend une valeur Writer w af Writer w a et on utilise un filtrage par motif via une expression letlet dessus. On présente le yy comme nouveau résultat et on utilise mappend pour combiner l’ancienne valeur monoïdalemappend avec la nouvelle. On replace ceci et le résultat dans un constructeur WriterWriter afin que notre résultat soit bien une valeur WriterWriter et pas simplement un tuple non encapsulé.

Qu’en est-il de returnreturn ? Elle doit prendre une valeur et la placer dans un contexte minimal qui retourne ce résultat. Quel serait un tel contexte pour une valeur

Writer

Writer ? Si l’on souhaite que notre valeur monoïdale affecte aussi faiblement que possible les autres valeurs monoïdales, il est logique d’utiliser mempty

mempty . memptymempty est l’élément neutre des valeurs monoïdales, comme """" ou Sum 0Sum 0 ou une chaîne d’octets vide. Quand on utilise mappendmappend avec mempty et une autre valeur monoïdale, le résultat est égal à cette autre valeur. Ainsi, si l’on utilise returnmempty return pour créer une valeur WriterWriter et qu’on utilise >>=>>= pour donner cette valeur à une fonction, la valeur monoïdale résultante sera uniquement ce que la fonction retourne. Utilisons

return

return sur le nombre 33 quelques fois, en lui attachant un monoïde différent à chaque fois :

ghci> runWriter (return 3 :: Writer String Int) (3,"")

ghci> runWriter (return 3 :: Writer (Sum Int) Int) (3,Sum {getSum = 0})

ghci> runWriter (return 3 :: Writer (Product Int) Int) (3,Product {getProduct = 1})

Puisque Writer n’a pas d’instance de ShowWriter Show , on a dû utiliser runWriterrunWriter pour convertir nos valeurs WriterWriter en tuples normaux qu’on peut alors afficher. Pour les StringString , la valeur monoïdale est la chaîne vide. Avec SumSum , c’est 00 , parce que si l’on ajoute 0 à quelque chose, cette chose est inchangée. Pour ProductProduct , le neutre est 11 .

L’instance Writer n’a pas d’implémentation de failWriter fail , donc si un filtrage par motif échoue dans une notation dodo , errorerror est appelée. Utiliser la notation do avec Writer

valeurs WriterWriter et qu’on veut faire quelque chose avec. Comme les autres monades, on peut les traiter comme des valeurs normales et les contextes sont pris en compte pour nous. Dans ce cas, les valeurs monoïdales sont attachées et mappendmappend les unes aux autres et ceci se reflète dans le résultat final. Voici un exemple simple de l’utilisation de la notation dodo avec WriterWriter pour multiplier des nombres.

import Control.Monad.Writer

logNumber :: Int -> Writer [String] Int

logNumber x = Writer (x, ["Got number: " ++ show x])

multWithLog :: Writer [String] Int

multWithLog = do a <- logNumber 3

b <- logNumber 5

return (a*b)

logNumber

logNumber prend un nombre et crée une valeur WriterWriter . Pour le monoïde, on utilise une liste de chaînes de caractères et on donne au nombre une liste singleton qui dit simplement qu’on a ce nombre. multWithLogmultWithLog est une valeur WriterWriter qui multiplie 33 et 55 et s’assure que leurs registres attachés sont inclus dans le registre final. On utilise returnreturn pour présenter a*ba*b comme résultat. Puisque returnreturn prend simplement quelque chose et le place dans un contexte minimal, on peut être sûr de ne rien avoir ajouté au registre. Voici ce qu’on voit en évaluant ceci :

ghci> runWriter multWithLog

(15,["Got number: 3","Got number: 5"])

Parfois, on veut seulement inclure une valeur monoïdale à partir d’un endroit donné. Pour cela, la fonction tell est utile. Elle fait partie de latell classe de types MonadWriterMonadWriter et dans le cas de WriterWriter , elle prend une valeur monoïdale, comme ["This is going on"]["This is going on"] et crée une valeur WriterWriter qui présente la valeur factice ()() comme son résultat, mais avec notre valeur monoïdale attachée. Quand on a une valeur monoïdale qui a un ()() en résultat, on ne le lie pas à une variable. Voici multWithLogmultWithLog avec un message supplémentaire rapporté dans le registre :

multWithLog :: Writer [String] Int

multWithLog = do a <- logNumber 3

b <- logNumber 5

tell ["Gonna multiply these two"] return (a*b)

Il est important que return (a*b)return (a*b) soit la dernière ligne, parce que le résultat de la dernière ligne d’une expression dodo est le résultat de l’expression entière. Si l’on avait placé telltell à la dernière ligne, ()() serait le résultat de l’expression dodo . On aurait perdu le résultat de la multiplication. Cependant, le registre serait le même. Place à l’action :

ghci> runWriter multWithLog

(15,["Got number: 3","Got number: 5","Gonna multiply these two"])

Ajouter de la tenue de registre à nos programmes

L’algorithme d’Euclide est un algorithme qui prend deux nombres et calcule leur plus grand commun diviseur. C’est-à-dire, le plus grand nombre qui divise à la fois ces deux nombres. Haskell contient déjà la fonction gcdgcd , qui calcule exactement ceci, mais implémentons la nôtre avec des capacités de registre. Voici l’algorithme normal :

gcd' :: Int -> Int -> Int

gcd' a b

| b == 0 = a

| otherwise = gcd' b (a `mod` b)

L’algorithme est très simple. D’abord, il vérifie si le second nombre est 0. Si c’est le cas, alors le premier est le résultat. Sinon, le résultat est le plus grand commun diviseur du second nombre et du reste de la division du premier par le second. Par exemple, si l’on veut connaître le plus grand commun diviseur de 8 et 3, on suit simplement cet algorithme. Parce que 3 est différent de 0, on doit trouver le plus grand commun diviseur de 3 et 2 (car si l’on divise 8 par 3, il reste 2). Ensuite, on cherche le plus grand commun diviseur de 3 et 2. 2 est différent de 0, on obtient donc 2 et 1. Le second nombre n’est toujours pas 0, alors on lance l’algorithme à nouveau sur 1 et 0, puisque diviser 2 par 1 nous donne un reste de 0.