• Aucun résultat trouvé

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 tels types. Le module Control.Monad.Writer exporte le type Writer w a ainsi que son instance de Monad 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 est juste un enrobage newtype de cela. Sa définition est très simple :

newtype Writer w a = Writer { runWriter :: (a, w) }

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

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,

seulement à présent que notre tuple est enveloppé dans un newtype Writer, on doit l’en sortir en filtrant par motif. On prend la valeur x et applique la fonction f. Cela nous rend une valeur

Writer w a et on utilise un filtrage par motif via une expression let dessus. On présente le y comme nouveau résultat et on utilise mappend pour combiner l’ancienne valeur monoïdale avec la nouvelle. On replace ceci et le résultat dans un constructeur Writer afin que notre résultat soit bien une valeur Writer et pas simplement un tuple non encapsulé.

Qu’en est-il de return ? 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 ? 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 est l’élément neutre des valeurs monoïdales, comme "" ou Sum 0 ou une chaîne d’octets vide. Quand on utilise mappend avec mempty et une autre valeur monoïdale, le résultat est égal à cette autre valeur. Ainsi, si l’on utilise return pour créer une valeur Writer 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 sur le nombre 3 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 Show, on a dû utiliser runWriter pour convertir nos valeurs Writer en tuples normaux qu’on peut alors afficher. Pour les String, la valeur monoïdale est la chaîne vide. Avec Sum , c’est 0, parce que si l’on ajoute 0 à quelque chose, cette chose est inchangée. Pour Product, le neutre est 1.

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

Utiliser la notation do avec Writer

À présent qu’on a une instance de Monad, on est libre d’utiliser la notation do pour les valeurs Writer. C’est pratique lorsqu’on a plusieurs valeurs Writer 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 mappend les unes aux autres et ceci se reflète dans le résultat final. Voici un exemple simple de l’utilisation de la notation do avec Writer 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 prend un nombre et crée une valeur Writer . 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. multWithLog est une valeur Writer qui multiplie 3 et 5 et s’assure que leurs registres attachés sont inclus dans le registre final. On utilise return pour présenter a*b comme résultat. Puisque return 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 la classe de types MonadWriter et dans le cas de Writer , elle prend une valeur monoïdale, comme ["This is going on"] et crée une valeur Writer 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 multWithLog 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) soit la dernière ligne, parce que le résultat de la dernière ligne d’une expression do est le résultat de l’expression entière. Si l’on avait placé tell à la dernière ligne, () serait le résultat de l’expression do. 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"])