• Aucun résultat trouvé

Trempons-nous les pieds avec Maybe

Maintenant qu’on a une vague idée de ce que sont les monades, voyons si l’on peut éclaircir cette notion un tant soit peu.

Sans surprise, Maybe est une monade, alors explorons cela encore un peu et voyons si l’on peut combiner cela avec ce qu’on sait des monades.

Soyez certain de bien comprendre les foncteurs applicatifs à présent. C‘est bien si vous ressentez comment les instances d’ Applicative fonctionnent et le genre de calculs qu’elles représentent, parce que les monades ne font que prendre nos connaissances des foncteurs applicatifs, et les améliorer.

Une valeur ayant pour type Maybe a représente une valeur de type a avec le contexte de l’échec potentiel attaché. Une valeur Just "dharma" signifie que la chaîne de caractères "dharma" est présente, alors qu’une valeur Nothing représente son absence, ou si vous imaginez la chaîne de caractères comme le résultat d’un calcul, cela signifie que le calcul a échoué.

Quand on a regardé Maybe comme un foncteur, on a vu que si l’on veut fmap une fonction sur celui-ci, elle est mappée sur ce qui est à l’intérieur des valeurs Just, les Nothing étant conservés tels quels parce qu’il n’y a pas de valeur sur laquelle mapper !

Ainsi :

ghci> fmap (++"!") (Just "wisdom") Just "wisdom!"

ghci> fmap (++"!") Nothing Nothing

En tant que foncteur applicatif, cela fonctionne similairement. Cependant, les foncteurs applicatifs prennent une fonction enveloppée. Maybe est un foncteur applicatif de manière à ce que lorsqu’on utilise <*> pour appliquer une fonction dans un Maybe à une valeur dans un Maybe, elles doivent toutes deux être des valeurs Just pour que le résultat soit une valeur Just, autrement le résultat est Nothing. C’est logique, puisque s’il vous manque soit la fonction, soit son paramètre, vous ne pouvez pas l’inventer, donc vous devez propager l’échec :

ghci> Just (+3) <*> Just 3 Just 6

ghci> Nothing <*> Just "greed" Nothing

ghci> Just ord <*> Nothing Nothing

Quand on utilise le style applicatif pour appliquer des fonctions normales sur des valeurs Maybe, c’est similaire. Toutes les valeurs doivent être des Just, sinon le tout est Nothing !

ghci> max <$> Just 3 <*> Just 6 Just 6

ghci> max <$> Just 3 <*> Nothing Nothing

À présent, pensons à ce que l’on ferait pour faire >>= sur Maybe. Comme on l’a dit, >>= prend une valeur monadique, et une fonction qui prend une valeur normale et retourne une valeur monadique, et parvient à appliquer cette fonction à la valeur monadique. Comment fait-elle cela, si la fonction n’accepte qu’une valeur normale ? Eh bien, pour ce faire, elle doit prendre en compte le contexte de la valeur monadique.

Dans ce cas, >>= prendrait un Maybe a et une fonction de type a -> Maybe b et appliquerait la fonction au Maybe a. Pour comprendre comment elle fait cela, on peut utiliser notre intuition venant du fait que Maybe est un foncteur applicatif. Mettons qu’on ait une fonction \x -> Just (x + 1). Elle prend un nombre, lui ajoute 1 et l’enveloppe dans un Just :

ghci> (\x -> Just (x+1)) 1 Just 2

ghci> (\x -> Just (x+1)) 100 Just 101

Si on lui donne 1, elle s’évalue en Just 2. Si on lui donne le nombre 100, le résultat est Just 101. Très simple. Voilà l’obstacle : comment donner une valeur Maybe a cette fonction ? Si on pense à ce que fait Maybe en tant que foncteur applicatif, répondre est plutôt simple. Si on lui donne une valeur Just , elle prend ce qui est dans le Just et applique la fonction dessus. Si on lui donne un Nothing, hmm, eh bien, on a une fonction, mais rien pour l’appliquer. Dans ce cas, faisons juste comme avant et retournons Nothing.

Plutôt que de l’appeler >>=, appelons-la applyMaybe pour l’instant. Elle prend un Maybe a et une fonction et retourne un Maybe b, parvenant à appliquer la fonction sur le Maybe a. Voici le code :

applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b

applyMaybe Nothing f = Nothing

applyMaybe (Just x) f = f x

Ok, à présent jouons un peu. On va l’utiliser en fonction infixe pour que la valeur Maybe soit sur le côté gauche et la fonction sur la droite :

ghci> Just 3 `applyMaybe` \x -> Just (x+1) Just 4

ghci> Just "smile" `applyMaybe` \x -> Just (x ++ " :)") Just "smile :)"

ghci> Nothing `applyMaybe` \x -> Just (x+1) Nothing

ghci> Nothing `applyMaybe` \x -> Just (x ++ " :)") Nothing

Dans l’exemple ci-dessus, on voit que quand on utilisait applyMaybe avec une valeur Just et une fonction, la fonction était simplement appliquée dans le Just. Quand on essayait de l’utiliser sur un Nothing, le résultat était Nothing. Et si la fonction retourne Nothing ? Voyons :

ghci> Just 3 `applyMaybe` \x -> if x > 2 then Just x else Nothing Just 3

ghci> Just 1 `applyMaybe` \x -> if x > 2 then Just x else Nothing Nothing

Comme prévu. Si la valeur monadique à gauche est Nothing, le tout est Nothing. Et si la fonction de droite retourne Nothing, le résultat est également Nothing . C’est très similaire au cas où on utilisait Maybe comme foncteur applicatif et on obtenait un Nothing en résultat lorsqu’il y avait un Nothing quelque part.

Il semblerait que, pour Maybe, on ait trouvé un moyen de prendre une valeur spéciale et de la donner à une fonction qui prend une valeur normale et en retourne une spéciale. On l’a fait en gardant à l’esprit qu’une valeur Maybe représentait un calcul pouvant échouer.

Vous vous demandez peut-être à quoi bon faire cela ? On pourrait croire que les foncteurs applicatifs sont plus puissants que les monades, puisqu’ils nous permettent de prendre une fonction normale et de la faire opérer sur des valeurs dans des contextes. On verra que les monades peuvent également faire ça, car elles sont des améliorations des foncteurs applicatifs, et qu’elles peuvent également faire des choses cool dont les foncteurs applicatifs sont incapables.

On va revenir à Maybe dans une minute, mais d’abord, découvrons la classe des types monadiques.

La classe de types Monad

Tout comme les foncteurs ont la classe de types Functor et les foncteurs applicatifs ont la classe de types Applicative, les monades viennent avec leur propre classe de types : Monad ! Wow, qui l’eut cru ? Voici à quoi ressemble la classe de types :

class Monad m where

return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b x >> y = x >>= \_ -> y fail :: String -> m a fail msg = error msg

Commençons par la première ligne. Elle dit class Monad m where . Mais, attendez, n’avons-nous pas dit que les monades étaient des foncteurs applicatifs améliorés ? Ne devrait-il pas y avoir une

contrainte de classe comme class (Applicative m) => Monad m where afin qu’un type doive être un foncteur applicatif pour être une monade ? Eh bien, ça devrait être le cas, mais quand Haskell a été créé, il n’est pas apparu à ses créateurs que les foncteurs applicatifs étaient intéressants pour Haskell, et donc ils n’étaient pas présents. Mais soyez rassuré, toute monade est un foncteur

applicatif, même si la déclaration de classe Monad ne l’indique pas.

La première fonction de la classe de types Monad est return. C’est la même chose que pure, mais avec un autre nom. Son type est (Monad m) => a -> m a . Elle prend une valeur et la place dans un contexte minimal contenant cette valeur. En d’autres termes, elle prend une valeur et l’enveloppe dans une monade. Elle est toujours définie comme pure de la classe de types Applicative, ce qui signifie qu’on la connaît déjà. On a déjà utilisé return quand on faisait des entrées-sorties. On s’en servait pour prendre une valeur et créer une I/O factice, qui ne faisait rien de plus que renvoyer la valeur. Pour Maybe elle prend une valeur et l’enveloppe dans Just.

Un petit rappel : return n’est en rien comme le return qui existe dans la plupart des langages. Elle ne termine pas l’exécution de la fonction ou quoi que ce soit, elle prend simplement une valeur normale et la place dans un contexte.

La prochaine fonction est >>=, ou bind. C’est comme une application de fonction, mais au lieu de prendre une valeur normale pour la donner à une fonction normale, elle prend une valeur monadique (dans un contexte) et alimente une fonction qui prend une valeur normale et retourne une valeur monadique.

Ensuite, il y a >>. On n’y prêtera pas trop attention pour l’instant parce qu’elle vient avec une implémentation par défaut, et qu’on ne l’implémente presque jamais quand on crée des instances de Monad.

La dernière fonction de la classe de types Monad est fail. On ne l’utilise jamais explicitement dans notre code. En fait, c’est Haskell qui l’utilise pour permettre les échecs dans une construction syntaxique pour les monades que l’on découvrira plus tard. Pas besoin de se préoccuper de

fail pour l’instant.

À présent qu’on sait à quoi la classe de types Monad ressemble, regardons comment Maybe est instancié comme Monad !

instance Monad Maybe where

return x = Just x

Nothing >>= f = Nothing Just x >>= f = f x fail _ = Nothing

return est identique à pure , pas besoin de réfléchir. On fait comme dans Applicative et on enveloppe la valeur dans Just .

La fonction >>= est identique à notre applyMaybe. Quand on lui donne un Maybe a, on se souvient du contexte et on retourne Nothing si la valeur à gauche est Nothing, parce qu’il n’y a pas de valeur sur laquelle appliquer la fonction. Si c’est un Just, on prend ce qui est à l’intérieur et on applique f dessus. On peut jouer un peu avec la monade Maybe :

ghci> return "WHAT" :: Maybe String Just "WHAT"

ghci> Just 9 >>= \x -> return (x*10) Just 90

ghci> Nothing >>= \x -> return (x*10) Nothing

Rien de neuf ou d’excitant à la première ligne puisqu’on a déjà utilisé pure avec Maybe et on sait que return est juste pure avec un autre nom. Les deux lignes suivantes présentent un peu mieux >>=.

Remarquez comment, lorsqu’on donne Just 9 à la fonction \x -> return (x*10), le x a pris la valeur 9 dans la fonction. Il semble qu’on ait réussi à extraire la valeur du contexte Maybe sans avoir recours à du filtrage par motif. Et on n’a pas perdu le contexte de notre valeur Maybe, parce que quand elle vaut

Nothing , le résultat de >>= sera également Nothing .

Le funambule

À présent qu’on sait comment donner un Maybe a à une fonction ayant pour type a -> Maybe b tout en tenant compte du contexte de l’échec potentiel, voyons comment on peut utiliser >>= de

manière répétée pour gérer le calcul de plusieurs valeurs Maybe a.

Pierre a décidé de faire une pause dans son emploi au centre de pisciculture pour s’adonner au funanbulisme. Il n’est pas trop mauvais, mais il a un problème : des oiseaux n’arrêtent pas d’atterrir sur sa perche d’équilibre ! Ils viennent se reposer un instant, discuter avec leurs amis aviaires, avant de décoller en quête de miettes de pain. Cela ne le dérangerait pas tant si le nombre d’oiseaux sur le côté gauche de la perche était égal à celui sur le côté droit. Mais parfois, tous les oiseaux décident qu’ils préfèrent le même côté, et ils détruisent son équilibre, l’entraînant dans une chute embarrassante (il a un filet de sécurité).

Disons qu’il arrive à tenir en équilibre tant que le nombre d’oiseaux sur les côtés gauche et droit ne s’éloignent pas de plus de trois. Ainsi, s’il y a un oiseau à droite et quatre oiseaux à gauche, tout va bien. Mais si un cinquième oiseau atterrit sur sa gauche, il perd son équilibre et plonge malgré lui. Nous allons simuler des oiseaux atterrissant et décollant de la perche et voir si Pierre tient toujours après un certain nombre d’arrivées et de départs. Par exemple, on souhaite savoir ce qui arrive à Pierre si le premier oiseau arrive sur sa gauche, puis quatre oiseaux débarquent sur sa droite, et enfin l’oiseau sur sa gauche décide de s’envoler.

On peut représenter la perche comme une simple paire d’entiers. La première composante compte les oiseaux sur sa gauche, la seconde ceux sur sa droite :

type Birds = Int

type Pole = (Birds,Birds)

Tout d’abord, on crée un synonyme du type Int, appelé Birds, parce qu’on va utiliser des entiers pour représenter un nombre d’oiseaux. Puis, on crée un synonyme pour (Birds, Birds) qu’on appelle Pole (à ne pas confondre avec une personne d’origine polonaise).

Ensuite, pourquoi ne pas créer une fonction qui prenne un nombre d’oiseaux et les fait atterrir sur un côté de la perche ? Voici les fonctions :

landLeft :: Birds -> Pole -> Pole

landLeft n (left,right) = (left + n,right)

landRight :: Birds -> Pole -> Pole

landRight n (left,right) = (left,right + n)

Simple. Essayons-les : ghci> landLeft 2 (0,0) (2,0) ghci> landRight 1 (1,2) (1,3) ghci> landRight (-1) (1,2) (1,1)

Pour faire décoller les oiseaux, on a utilisé un nombre négatif. Puisque faire atterrir des oiseaux sur un Pole retourne un Pole, on peut chaîner les applications de landLeft et landRight :

ghci> landLeft 2 (landRight 1 (landLeft 1 (0,0))) (3,1)

Quand on applique la fonction landLeft 1 sur (0, 0), on obtient (1, 0). Puis, on fait atterrir un oiseau sur le côté droit, ce qui donne (1, 1). Finalement, deux oiseaux atterrissent à gauche, donnant (3, 1). On applique une fonction en écrivant d’abord la fonction puis le paramètre, mais ici, il serait plus pratique d’avoir la perche en première et ensuite la fonction d’atterrissage. Si l’on crée une fonction :

x -: f = f x

On peut appliquer les fonctions en écrivant d’abord le paramètre puis la fonction :

ghci> 100 -: (*3) 300

ghci> True -: not False

ghci> (0,0) -: landLeft 2 (2,0)

Ainsi, on peut faire atterrir de façon répétée des oiseaux, de manière plus lisible :

ghci> (0,0) -: landLeft 1 -: landRight 1 -: landLeft 2 (3,1)

Plutôt cool ! Cet exemple est équivalent au précédent où l’on faisait atterrir plusieurs fois des oiseaux sur la perche, mais il a l’air plus propre. Ici, il est plus facile de voir qu’on commence avec (0, 0) et qu’on fait atterrir un oiseau à gauche, puis un à droite, puis deux à gauche.

Jusqu’ici, tout est bien. Mais que se passe-t-il lorsque 10 oiseaux atterrissent du même côté ?

ghci> landLeft 10 (0,3) (10,3)

10 oiseaux à gauche et seulement 3 à droite ? Avec ça, le pauvre Pierre est sûr de se casser la figure ! Ici, c’est facile de s’en rendre compte, mais si l’on avait une séquence comme :

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2) (0,2)

On pourrait croire que tout va bien, mais si vous suivez attentivement les étapes, vous verrez qu’à un moment il y a 4 oiseaux sur la droite et aucun à gauche ! Pour régler ce problème, il faut retourner à nos fonctions landLeft et landRight. De ce qu’on voit, on veut que ces fonctions puissent échouer. C’est-à-dire, on souhaite qu’elles retournent une nouvelle perche tant que l’équilibre est respecté, mais qu’elles échouent si les oiseaux atterrissent en disproportion. Et quel meilleur moyen d’ajouter un contexte de possible échec que d’utiliser une valeur Maybe ? Reprenons ces fonctions :

landLeft :: Birds -> Pole -> Maybe Pole

landLeft n (left,right)

| abs ((left + n) - right) < 4 = Just (left + n, right) | otherwise = Nothing

landRight :: Birds -> Pole -> Maybe Pole

landRight n (left,right)

| abs (left - (right + n)) < 4 = Just (left, right + n) | otherwise = Nothing

Plutôt que de retourner un Pole, ces fonctions retournent un Maybe Pole. Elles prennent toujours un nombre d’oiseaux et une perche existante, mais elles vérifient si l’atterrisage des oiseaux déséquilibre ou non Pierre. On a utilisé des gardes pour vérifier si la différence entre les extrémités de la perche est inférieure à 4. Si c’est le cas, on enveloppe la nouvelle perche dans un Just et on la retourne. Sinon, on retourne Nothing pour indiquer l’échec.

Mettons ces nouvelles-nées à l’épreuve :

ghci> landLeft 2 (0,0) Just (2,0)

ghci> landLeft 10 (0,3) Nothing

Joli ! Quand on fait atterrir des oiseaux sans détruire l’équilibre de Pierre, on obtient une nouvelle perche encapsulée dans un Just. Mais quand trop d’oiseaux arrivent d’un côté de la perche, on obtient Nothing. C’est cool, mais on dirait qu’on a perdu la capacité de chaîner des atterissages d’oiseaux sur la perche. On ne peut plus faire landLeft 1 (landRight 1 (0,0)) parce que lorsqu’on applique landRight 1 à (0, 0), on n’obtient pas un Pole mais un

Maybe Pole . landLeft 1 prend un Pole et retourne un Maybe Pole .

On a besoin d’une manière de prendre un Maybe Pole et de le donner à une fonction qui prend un Pole et retourne un Maybe Pole. Heureusement, on a >>= , qui fait exactement ça pour Maybe . Essayons :

ghci> landRight 1 (0,0) >>= landLeft 2 Just (2,1)

Souvenez-vous, landLeft 2 a pour type Pole -> Maybe Pole. On ne pouvait pas lui donner directement un Maybe Pole résultant de

landRight 1 (0, 0) , alors on utilise >>= pour prendre une valeur dans un contexte et la passer à landLeft 2 . >>= nous permet en effet de traiter la valeur Maybe comme une valeur avec un contexte, parce que si l’on donne Nothing à landLeft 2 , on obtient bien Nothing et l’échec est propagé :

ghci> Nothing >>= landLeft 2 Nothing

valeur normale.

Voici une séquence d’atterrisages :

ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2 Just (2,4)

Au début, on utilise return pour prendre une perche et l’envelopper dans un Just. On aurait pu appeler directement landRight 2 sur (0, 0), c’était la même chose, mais la notation est plus consistente en utilisant >>= pour chaque fonction. Just (0, 0) alimente landRight 2, résultant en Just (0, 2). À son tour, alimentant landLeft 2, résultant en Just (2, 2), et ainsi de suite.

Souvenez-vous de l’exemple précédent où l’on introduisait la notion d’échec dans la routine de Pierre :

ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2) (0,2)

Cela ne simulait pas très bien son interaction avec les oiseaux, parce qu’au milieu, il perdait son équilibre, mais le résultat ne le reflétait pas. Redonnons une chance à cette fonction en utilisant l’application monadique ( >>=) plutôt que l’application normale.

ghci> return (0,0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2) Nothing

Génial. Le résultat final représente l’échec, qui est ce qu’on attendait. Regardons comment ce résultat est obtenu. D’abord, return place (0, 0) dans un contexte par défaut, en faisant un Just (0, 0). Puis,

Just (0,0) >>= landLeft 1 a lieu. Puisque Just (0, 0) est une valeur Just , landLeft 1 est appliquée à (0, 0) , retournant Just (1, 0) , parce que l’oiseau seul ne détruit pas l’équilibre. Ensuite,

Just (1,0) >>= landRight 4 prend place, et le résultat est Just (1, 4) car l’équilibre reste intact, bien qu’à sa limite. Just (1, 4) est donné à landLeft (-1). Cela signifie que landLeft (-1) (1, 4) prend place.

Conformément au fonctionnement de landLeft, cela retourne Nothing, parce que la perche résultante est déséquilibrée. À présent on a un Nothing qui est donné à landRight (-2), mais parce que c’est un Nothing, le résultat est automatiquement Nothing, puisqu’on n’a rien sur quoi appliquer

landRight (-2) .

On n’aurait pas pu faire ceci en utilisant Maybe comme un foncteur applicatif. Si vous essayez, vous vous retrouverez bloqué, parce que les foncteurs applicatifs ne permettent pas que les valeurs applicatives interagissent entre elles. Au mieux, elles peuvent être utilisées en paramètre d’une fonction en utilisant le style applicatif. Les opérateurs applicatifs iront chercher leurs résultats, et les passer à la fonction de façon appropriée en fonction du foncteur applicatif, puis placeront les valeurs applicatives résultantes ensemble, mais il n’y aura que peu d’interaction entre elles. Ici, cependant, chaque étape dépend du résultat de la

précédente. À chaque atterrissage, le résultat possible de l’atterrissage précédent est examiné pour voir l’équilibre de la perche. Cela détermine l’échec ou non de l’atterrissage.

On peut aussi créer une fonction qui ignore le nombre d’oiseaux sur la perche et fait tomber Pierre. On peut l’appeler banana :