Le Haskell, un langage au label pure. TroisiĂšme partie.

Et voici la troisiĂšme et derniĂšre partie de notre dĂ©couverte de ce joli langage. Au programme : des super foncteurs. D’abord des foncteurs applicatifs, puis des monades! Si vous rĂȘviez de savoir pourquoi tant de gens s’enflamment devant ce langage, il est peut-ĂȘtre venue l’heure de la rĂ©vĂ©lation.

La premiĂšre partie est ici, la seconde lĂ .

Foncteurs applicatifs

Les foncteurs applicatifs sont un enrichissement des foncteurs. C’est donc une classe, dĂ©finie dans le module Control.Applicative, de la façon suivante :

class (Functor f) => Applicative f where
   pure  :: a -> f a
   (<*>) :: f (a -> b) -> f a -> f b

Avec un type fonctoriel Fonc, on pouvais :

Grace Ă  un foncteur applicatif, on peut :

La seconde notion est plus gĂ©nĂ©rale, car si vous avez un constructeur de type Fonc qui est une instance de Applicative (La classe des foncteurs applicatifs), alors c’est un foncteur de la façon suivante :

instance Functor Fonc where
   fmap f e = (pure f) <*> e

C’est Ă  dire : Prenez f une fonction de type a -> b, alors vous savez comment l’appliquer sur un Fonc a. Il suffit de la “placer” elle aussi dans un foncteur, pour se retrouver avec une “fonction dans un contexte” de type Fonc (a->b) Ă  appliquer sur un â€œĂ©lĂ©ment dans un contexte” de type F a.

C’est donc pour cela que l’on impose Ă  tout foncteur applicatif d’ĂȘtre d’abord une instance de Functor, et que vous avez la contraire "Functor f" dans la dĂ©finition de la classe Applicative.

On se prépare au grand saut avec Maybe

Tout ceci est trÚs abstrait, alors regardons ce que cela donne avec un exemple simple. On considÚre Maybe qui est une instance de Applicative de la façon suivante :

instance Applicative Maybe where
   pure = Just
   (Just f) <*> (Just x) = Just (f x)
   _        <*> _        = Nothing

La mĂ©thode pure prend un truc :: a et le place dans un Maybe a par Just truc. C’est la façon la plus intuitive de placer notre truc dans un Maybe, sans perdre d’information. L’opĂ©rateur <*> se contente lui de rĂ©cupĂ©rer la fonction Ă  sa gauche, la valeur Ă  sa droite, d’effectuer le calcul (l’application de f Ă  x) puis de placer le rĂ©sultat dans un contexte minimal en utilisant le constructeur Just. La derniĂšre ligne s’occupe des cas oĂč il manque la fonction, l’argument, ou bien les deux. Dans ces trois cas, on ne peut effectuer le calcul, et l’on ne peut donc pas produire de rĂ©sultat.

Comme dit plus haut, si l’on vais une fonction qui prend la racine carrĂ© (sqrt :: Int) d’un entier, et “peut-ĂȘtre un nombre” (un v :: Maybe Int), on pouvais mapper notre fonction avec la ligne fmap sqrt v. Avec un foncteur applicatif, on peut aussi Ă©crire :

resultat  = (pure sqrt) <*> v

Mais alors, quel est l’intĂ©rĂȘt des foncteurs applicatifs? Un premier exemple est l’application d’une fonction prenant trois entiers Ă  trois “peut-ĂȘtre un entier”.

--Ici, v1, v2 et v3 sont de type Maybe Int.
--Si vous voulez savoir d'oĂč il viennent, disons que ce sont le rĂ©sultat
--de la lecture d'une chaine de caractĂšre et de tentative de conversion de
--la chaine en nombres. Si c'Ă©tait possible, alors on a des valeurs. Sinon, on auras "Nothing".

--On a une superbe fonction :
deepThought :: Int -> Int -> Int -> Answer

--Et on veut l'appliquer Ă  v1, v2, v3.
--Si l'on essaye avec un foncteur :
premiereApplication :: Maybe (Int->Int->Answer)
premiereApplication = fmap deepThought v1

--Maintenant, on est bloquer, on ne sais pas appliquer
-- une fonction qui se trouve dans un maybe...
--... Ă  moins d'utiliser <*> :)
secondeApplication :: Maybe (Int -> Answer)
secondeApplication = premiereApplication <*> v2
derniereApplication :: Maybe Answer
derniereApplication = secondeApplication <*> v3

--En fait, on aurait pu d'abord placer la fonction dans un Just,
-- puis appliquer v1, v2 et v3 avec <*> :
toutEnUn :: Maybe Answer
toutEnUn = (pure deepThought) <*> v1 <*> v2 <*> v3

--Et pour finir, sachez qu'il existe un petit sucre syntaxique
-- pour "(pure f) <*> x" ; l'opérateur <$> :
toutEnUn :: Maybe Answer
toutEnUn' = deepThought <$> v1 <*> v2 <*> v3

Mais parce que le haskell détient bien plus de P-P-P-P-Puissance, intéressons nous à une autre instance de Applicative : Les listes.

Premier plongeon avec les listes

Comme nous l’avions vu la derniĂšre fois, les listes sont un excellent exemple de foncteur. Mais pouvons nous en faire des foncteurs applicatifs? Il est facile de placer une valeur dans un contexte minimal : on dĂ©finira pure a = [a]. En fait, il nous faudrait rĂ©pondre Ă  la question suivante : Si l’on a une liste de fonction [f1, f2, f3] et de valeurs [2, 3, 4], comment dĂ©finir l’opĂ©rateur <*> ?

La solution retenue par haskell est on ne peut plus simple : on applique toutes les fonctions Ă  toutes les valeurs. Voici la dĂ©finition de l’instance :

instance Applicative [] where
    pure x = [x]
    fs <*> xs = [f x | f <- fs, x <- xs]

Cela donne une façon trĂšs facile d’effectuer de nombreux calculs simultanĂ©ment ; vous disposez d’un ensemble de valeurs, et d’un ensemble de fonctions. Vous voulez connaĂźtre TOUS les rĂ©sultats possible. Il suffit alors d’appliquer la liste des fonctions sur la liste des valeurs avec l’opĂ©rateur <*>. Un exemple, en partie tirĂ© de_Learn You Haskell for Great Good_ est le problĂšme du parcoure d’un cavalier. Un cavalier est situĂ© sur une case d’un Ă©chiquier infini, et vous voulez connaĂźtre toute les positions oĂč il peut se trouver aprĂšs 5 coups. Il suffit dĂ©crire une fonction par dĂ©placement possible, puis de construire une liste de ces fonctions, disons [u1, u2, l1, l2, r1, r2, d1, d2]. On applique cette liste Ă  la position d’origine placĂ©e dans un contexte : [(x, y)] (ou encore pure (x, y)). La solution est donnĂ© par l’application rĂ©pĂ©tĂ© de notre liste de fonction, comme le montre le code suivant.

u1 (l, c) = (l+2,c+1)
u2 (l, c) = (l+2,c-1)
d1 (l, c) = (l-2,c+1)
d2 (l, c) = (l-2,c-1)
l1 (l, c) = (l+1,c-2)
l2 (l, c) = (l-1,c-2)
r1 (l, c) = (l+1,c+2)
r2 (l, c) = (l-1,c+2)

fctListe = [u1, u2, d1, d2, l1, l2, r1, r2]
origine (l, c) = [(l,c)]

etapeSuivante position = fctListe <*> position

solution (l, c) = etapeSuivante . etapeSuivante . etapeSuivante . etapeSuivante . etapeSuivante $ origine (l, c)

En apnée : le foncteur (->) r !

On les avais cacher lors de la discussion des foncteurs, car ils sont difficile Ă  cernĂ©. Leur intĂ©rĂȘt n’est pas Ă©vident au premier abord et leur construction est quelque peu
 surprenante. Mais puisqu’ils sont utile comme foncteur applicatif, parlons en! Si la partie la plus abstraite vous Ă©chappe, aucune raison de vous inquiĂ©ter, l’idĂ©e est de survoler les notions pour avoir un aperçu, Ă©veiller la curiositĂ© et inciter Ă  lire des livres/articles qui expliquent en dĂ©taille ce qui n’est ici que mentionner. Si tout cela vous intĂ©resse, sautez Ă  la section “Foncteurs applicatifs” de Learn You Haskell for Great Good!

L’opĂ©rateur -> est un constructeur de type, Ă  deux arguments. Vous lui donnez deux types, a et b, et il vous construiras le type “prend du a et retourne du b”. On peut donc Ă©crire f :: a -> b ou encore ` f :: (->) a b. Que signifie alors (->) r ? On parle d'un constructeur de type Ă  un argument qui, si vous lui donnez un type a, dĂ©signeras alors les fonctions de type a -> r. Si r dĂ©signe un type, on peut alors faire de (->) r une instance de Functor oĂč mapper une fonction f de type a->b sur une fonction g de type a -> r signifie appliquer f au rĂ©sultat de l'Ă©valuation de la fonction g`. Plus prĂ©cisĂ©ment :

instance Functor ((->) r) where
    fmap f g = (\x -> f (g x))

On peut se demander l’intĂ©rĂȘt, puisque (fmap f g) 42 se simplifie en f . g $ 42 qui est, Ă  priori, bien plus lisible. Outre sa fantastique capacitĂ© Ă  mettre votre esprit Ă  rude Ă©preuve, ce changement de point de vue devient trĂšs intĂ©ressant avec la classe Applicative, puisqu’il donne la possibilitĂ© d’avoir un ensemble de paramĂštres.

Un exemple commenté :

--Notre type "paramĂštre". On aurais pu construire une sorte de grosse structure
-- avec diverses informations.
--Dans cette exemple, on se contenteras d'un nombre.
type Param = Int

unEntier :: Param -> Int
unEntier = pure 5

unAutreEntier :: Param -> Int
unAutreEntier param = 5 + param

uneFonction :: Param -> Int - > String
uneFonction param arg = show (arg + param)

somme = pure (+) <$> unEntier <*> unAutreEntier
affichage = uneFonction <*> somme

--Vaut 94 :
evaluationDansUnCOntexte = affichage 42

Quelques rĂšgles que nul ne doit ignorer

Comme on dit, dura lex, sed lex. Les foncteurs devaient respecter certaines rĂšgles, et il en est de mĂȘme des foncteurs applicatifs. Une fois habituĂ© aux foncteurs applicatifs, ces rĂšgles semblent dĂ©couler du bon sens. Ce sont des invariants que DOIVENT respecter vos instances d’Applicative. Si vous ne les respectez pas, c’est que ce que vous voulez faire n’est pas un foncteur applicatif, et n’a donc aucune raison d’ĂȘtre instance d’Applicative.

Sans trop rentrer dans les détails, les voici, briÚvement commentés :

-- Neutre :
pure id <*> v = v
--Cela signifie qu'appliquer la fonction identité
-- (id = (\x -> x) ) a un élément v via <*> le laisse inchangé.
--C'est une sorte de "neutre Ă  gauche".

-- Composition :
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
--Cela signifie que composer les fonctions à l'intérieur de u et v
-- via l'opérateur .  (C'est la partie pure (.) <*> u <*> v) revient à calculer u sur le résultat de v.
--Comme on compare deux fonctions sur leur valeur, et que l'on parle de résultat, on doit introduire
-- un certain mister w, et on vĂ©rifie que quelque soit ce w, on a bien que u calculer sur v calculĂ© sur w donne bien le mĂȘme rĂ©sultat que la composĂ© (pure (.) <*> u <*> v) calculĂ© sur w.

-- Morphisme :
pure f <*> pure x = pure (f x)
--Cette égalité garanti que : placer f dans un contexte minimal, placer x dans un contexte
-- minimal, puis "mouliner" donne le mĂȘme rĂ©sultat que f x placĂ© dans un contexte minimal. En quelque sorte,
-- on transforme l'opérateur $ :: (a->b) -> a -> b en l'opérateur <*> :: f (a->b) -> f a -> f b.

-- ' Échange '
u <*> pure y = pure ($ y) <*> u

-- C'est une sorte de commutativité du pauvre. On ne peut pas vraiment échanger les arguments à droite et à gauche, car l'un est une fonction, l'autre une valeur. Mais on peut transformer une évaluation "f y" en "$ f y ", ce qui permet de changer l'ordre des arguments.

Monades

Les monades, c’est le cran au dessus. On ne veut plus seulement mapper des fonctions f: a -> b Ă  l’intĂ©rieur d’un Fonc a, ni seulement Ă©valuer des fonctions Fonc (a -> b) dans un contexte F a. Maintenant, on dispose de fonctions qui travaillent sur une valeur, et produisent un rĂ©sultat dans un contexte. Des fonctions de type f :: a -> Fonc b. Si l’on essayais de les mapper comme des foncteurs sur un Fonc a, on se retrouverais avec du Fonc (Fonc b), ce qui n’est pas du tout ce que l’on veut. Il nous faut donc une fonction capable de recoller ces “Fonc Fonc” en “Fonc”. C’est ce que fournisse les monades.

Tout de suite, la classe monade :

class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b -- On l’appelle aussi "bind"
  (>>) :: m a -> m b -> m b -- C'est une sorte d'opérateur de concaténation.
-- >> Ignore le premier argument et renvoi la valeur du second.
-- On vera plus tard qu'en fait, c'est extrĂȘmement utile, avec la monade IO.
  return :: a -> m a -- C'est notre bon vieux pure, sous un autre nom.
  fail :: String -> m a -- On ne l'utiliseras pas, et on en parleras pas.
-- Sachez toute fois que ca renvoi moralement un "contexte sans information".
-- Par exemple une liste vide, un Nothing, etc.

Les deux opĂ©rateurs principaux sont bind et return. Voyons comment on pourrait, partant d’une monade, la faire instance d’Applicative :

instance Monade Fonc where
  pure = return
  -- L'astuce est de construire une fonction f' :: (a -> m b) que l'on puisse utiliser Ă  la place de f.
  -- On la construit grĂące Ă  "pure . f".
  -- Mais comme f est elle mĂȘme dans un contexte, il faut faire cette transformation dans le contexte.
  mf (<*>) mv = mf >>= (\f -> mv >>= return.f)
-- On donne mf à manger a la grosse fonction de droite. La grosse fonction de droite récupÚre la fonction f, la transforme en une f' par return.f. On donne donc la valeur v contenue dans mv à manger a la fonction f' grùce à >>=.

Bon, si vous avez suivi jusque lĂ , soit vous connaissez dĂ©jĂ  le haskell, soit vous ĂȘtes des sur-hommes (ou des matheux, auquel cas je ne peux plus rien pour vous). Voyons en pratique ce qu’apportent les monades, et pourquoi est-ce que bien utilisĂ©, elles offres une nouvelle façon de rĂ©soudre certains problĂšmes bien connu du monde impĂ©ratif.

Être ou ne pas ĂȘtre?

Dans un “vrai” programme, on n’a pas toujours une valeur a retourner pour une fonction. Que faire si l’on demande le premier Ă©lĂ©ment d’une liste vide? Et si jamais on veut convertir une chaine en un nombre, qui par malheur contient le prĂ©nom de votre animal de compagnie? En bref, comment gĂ©rer une erreur correspondant Ă  l’absence d’un rĂ©sultat?

La rĂ©ponse est la monade maybe. Commençons par des fonctions qui renvoient peut-ĂȘtre une valeur :

maybeHead :: [a] -> Maybe a
maybeHead [] = Nothing
maybeHead (head : tail) = Just head

maybeList :: (Int, Int) -> Maybe [Int]
maybeList (first, last) = if first <= last then [first..last] else Nothing

maybeRange :: Bool -> Maybe (Int, Int)
maybeRange False = Nothing
maybeRange True = (23, 42)

On voudrais maintenant rĂ©cupĂ©rer le premier Ă©lĂ©ment de la liste pour les valeur donnĂ© par maybeRange, si la liste existe, bien sure. C’est la que les monades interviennent! GrĂące au monades, on peut composer les deux fonctions, bien qu’un Maybe [Int] ne soit pas un [Int].

resultat :: Maybe Int
resultat choice = maybeRange choice >>= maybeList >>= maybeHead

Si l’une des Ă©tapes ne produit pas de rĂ©sultat (un Nothing), alors l’absence de rĂ©sultat seras propagĂ© et on obtiendras un Nothing.

Cette mĂ©thode a de nombreux avantages par rapport aux deux vielles solutions bien connues : 1) Le “code d’erreur”, c’est Ă  dire placer nullptr quand on pointeur n’existe pas, ou encore “-1” ou 0 pour signaler une erreur. L’inconvĂ©nient de cette mĂ©thode est d’obliger le dĂ©veloppeur Ă  vĂ©rifier chacune des valeurs de retour avec un if, gĂ©nĂ©ralement pour sortir de la fonction, souvent en retournant un nouveau code d’erreur pour signaler que le rĂ©sultat produit n’est pas “vraiment” un rĂ©sultat, mais une absence de rĂ©sultat.

L’utilisation de la monade Maybe permet d’éviter ces testes rĂ©pĂ©tĂ©. Si une seul des fonctions ne peut pas fournir de rĂ©sultat, alors les applications suivantes seront toute ignorĂ© et, bien entendu, ces fonctions ne seront pas Ă©valuĂ©es, donc pas de coĂ»t en temps de calcul.

2) Les exceptions. Cela consiste Ă  interrompre l’exĂ©cution normale du programme pour remonter a travers toute la pile d’appels, en espĂ©rant que quelqu’un seras assez gentil pour s’occuper de cette erreur. Cela a un coĂ»t en terme de performances, et doit ĂȘtre rĂ©server pour les Ă©vĂšnements exceptionnels. L’impossibilitĂ© de produire un rĂ©sultat est rarement exceptionnel, c’est plutĂŽt chose commune.

Le chaĂźnage de monade Maybe a l’avantage de ne pas dĂ©clencher un erreurs qui pourrait se perdre et aller jusqu’à interrompre le programme. Que les valeurs soient prĂ©sente ou non, le comportement est toujours “simple” Ă  prĂ©dire. Et plus un code est simple, moins il y a de risque qu’une erreurs s’introduisse Ă  l’insu du dĂ©veloppeur.

En rĂšgle gĂ©nĂ©rale, dĂšs que le rĂ©sultat peut ne pas ĂȘtre fournit, vous devriez utiliser la monade Maybe. Si parfois une certaine fonction f que vous voulez chaĂźner produit toujours un rĂ©sultat, alors vous pouvez la placer au milieu d’une chaine de >>= en Ă©crivant return.f. Vous pouvez aussi une bonne vielle fmap, car toute les monades sont des foncteurs applicatifs, donc des foncteurs.

Nb : Peut-ĂȘtre avez vous besoin de conserver une information sur l’origine de l’erreur. Ceci est possible grĂące Ă  la monade Either a (souvent on utilise Either String pour stocker un message).

Pour finir, voici l’instance de cette monade :

instance Monad Maybe where  
        return x = Just x  
        Nothing >>= f = Nothing  
        Just x >>= f  = f x  
        fail _ = Nothing  

Un calcul pas trÚs déterministe

Nous avions vu comment les listes comme foncteurs applicatifs permettent de rĂ©soudre Ă©lĂ©gamment la question du dĂ©placement d’un cavalier. Mais dans un Ă©chiquier fini, on ne savais pas trop comment gĂ©rer les bords.

Les listes, vu comme monade, nous permettent de combiner des fonctions de type a -> [b]. L’idĂ©e est que vous disposez de diverse fonctions qui prennent une valeur, et produise divers rĂ©sultats possible. Vous voulez alors appliquer des fonctions sur chacun de ces rĂ©sultats. On peut donc parler de calcul non-dĂ©terministe : une valeur donne plusieurs rĂ©sultats possible.

On peut donc rĂ©aliser un remake du cavalier, en se servant de ce calcul non dĂ©terministe, puisqu’à partir d’une position, on a diverses positions possibles

--Quelques types pour plus de lisibilité,
-- histoire de rappeler que les types sont
-- aussi là pour fournir des informations sémantiques.
type Ligne = Int
type Collone = Int
type Position = (Ligne, Collone)

--Fonction utilisé pour ne garder que les positions dans l'échiquier
dansLechiquier :: Position -> Bool
dansLechiquier (l, c) = l `elem` [1..8] && c `elem` [1..8]

--Produit une liste des positions possibles
deplacerCavalier :: Position -> [Position]
deplacerCavalier (l, c) = filter dansLechiquier liste
  where liste = [(l+2,c-1),(l+2,c+1),(l-2,c-1),(l-2,c+1) 
                ,(l+1,c-2),(l+1,c+2),(l-1,c-2),(l-1,c+2)]

--Donne la liste des positions possible du cavalier, partant de (4, 5), et aprÚs trois déplacements.
resultat = return (4, 5) >>= deplacerCavalier >>= deplacerCavalier >>= deplacerCavalier

En fait, une autre façon de faire serai de mapper la fonction deplacerCavalier dans la liste de positions, produisant un [[Position]], puis d’appeler concat sur cette liste, la recollant en une [Position]. C’est d’ailleurs de cette façon qu’est implĂ©mentĂ© l’opĂ©rateur >>= !

L’utilisation des listes sous leur forme de monade n’est pas limitĂ© Ă  cette exemple. Je peux mentionner un second cas qui vous paraĂźtras peut-ĂȘtre plus concret. Disons que vous voulez enregistrer une image, et que vous disposez d’une fonction qui vous donne les couleurs r, g, b, a sous la forme d’une liste de nombres, c’est Ă  dire getPixel (x, y) :: [Word8] (Word8 est un nombre codĂ© sur 8 bits). Sachant que pour enregistrer l’image, vous devez fournir un tableau, qui peut ĂȘtre trĂšs facilement construit Ă  partir de la liste des valeurs qu’il vas contenir. (Le compilateur est trĂšs malins, et le programme compilĂ© ne s’amusera pas Ă  produire une liste, la construire en mĂ©moire, puis la placer dans le tableau. Pas d’inquiĂ©tude, le compilateur est trĂšs douer Ă  ce niveau.) La monade [] permet d’écrire en une ligne, de façon trĂšs Ă©lĂ©gante, la crĂ©ation d’un tableau oĂč les valeurs sont bien la succession des valeurs de getPixel calculĂ© Ă  chaque coordonnĂ©. Bien sur, on aurais pu utiliser concat et map, mais c’est justement ce que fait l’opĂ©rateur >>=. Tout ça pour dire que les listes vue comme monade ne sont pas un gadget, mais bien un outil utile Ă  l’implĂ©mentation d’applications de la “vie de tout les jouers”.

Voici la dĂ©finition de l’instance pour les curieux :

instance Monad [] where  
    return x = [x]  
    xs >>= f = concat (map f xs)  
    fail _ = []

Dou? Doo? Do, c’est le goĂ»t!

La notation do est une sorte de super sucre syntaxique. Seulement, les monades sont tellement amĂšre que vous aurez vraiment besoin de ce sucre, je vous l’assure. La notation do permet dĂ©crire facilement le chaĂźnage d’actions, et le fait de rĂ©cupĂ©rer des valeurs dans un contexte.

ConsidĂ©rons l’exemple suivant tirĂ© de Learn You Haskell for Great Good (Non, je ne suis pas encore sponsorisĂ© par eux.) :

foo :: Maybe String  
foo = Just 3   >>= (\x -> 
      Just "!" >>= (\y -> 
      Just (show x ++ y)))

On rĂ©cupĂšre deux valeurs de monades Ă  travers x et y, puis l’on place le rĂ©sultat de show x ++ y dans un contexte. C’est lourd a Ă©crire, demande l’imbrication de fonctions
 Avec la notation do, cela Ă©vident :

foo :: Maybe String  
foo = do  
        x <- Just 3  
        y <- Just "!"  
        Just (show x ++ y)  

-- Ou encore :
foo :: Maybe String  
foo = do  
        x <- Just 3  
        y <- Just "!"  
        return (show x ++ y)  

Dans les deux premiÚres lignes, on récupÚre x et y depuis Just 3 et Just "!", puis on fait notre traitement.

Hey! Mais ça ressemble a des listes en comprĂ©hension tout ça! Reproduisons le mĂȘme exercice mais avec des listes :

foo :: Maybe String  
foo = [1, 2, 3]   >>= (\x -> 
          [".", "!", "?"] >>= (\y -> 
          [show x ++ y]))

Le rĂ©sultat produit est toute les façons de coller l’un des signes de ponctuation aprĂšs l’un des nombres. C’est exactement la mĂȘme chose que :

foo :: Maybe String  
foo = do
          x <- [1, 2, 3]
          y <- [".", "!", "?"]
          return (show x ++ y)

-- Ou encore :
foo = [show x ++ y | x <- [1, 2, 3] | y <- [".", "!", "?"]]

VoilĂ  donc l’origine des liste en comprĂ©hension! Et oui, il nous aura fallu arriver jusqu’ici pour pouvoir enfin expliquer ce que sont les liste en comprĂ©hension. C’est simplement un sucre syntaxique spĂ©cifique au liste d’un bloque do. C’est donc de la manipulation de monade que vous faites, Ă  chaque fois que vous Ă©crivez une liste en comprĂ©hension. Si c’est si pratique avec les listes, vous vous doutez bien que pouvoir le faire avec diverses structures (des arbres par exemple), est tout aussi pratique.

Vous vous demandez alors comment ajouter les conditions, comme dans [x^2 | x <- [1..20], x mod 2 == 0] ? Et bien vous pouvez utilisez la fonction guard, qui produiras une liste vide si la condition n’est pas vĂ©rifiĂ©e :

foo = do
      x <- [1..20]
      guard (\x -> x `mod` 2 == 0)
      return x^2

Le retour de Jafar, aussi connu sous le nom de (->) r.

Nous avions utilisĂ© le foncteur applicatif (->) r pour reprĂ©senter des calculs qui dĂ©pendent d’un contexte. On savais donc appliquer des fonctions r -> (a -> b) sur des valeurs r -> a. Seulement, il est plus commun de partir d’une valeur, et produire un rĂ©sultat qui dĂ©pend du contexte. On voudrais donc une monade, pour pouvoir combiner des fonctions de type a -> (r -> b) (Les parenthĂšses sont lĂ  pour faire ressortir que l’on considĂšre (->) r comme un foncteur / une monade, mais bien sur facultatives).

Comme (->) r est l’une des monades les plus abstraites, regardons un exemple concret, oĂč le contexte est la position d’une camĂ©ra dans un raytracer.

-- On définit une caméra.
-- Une caméra contient la position depuis la quelle
-- les rayons sont lancé, la distance à la quel se trouve
-- le plan, et la taille de celui ci.
type Position = (Double, Double, Double)
type Direction = (Double, Double, Double)
type Distance = Double
type Coordonnee = Int
type Largeur = Coordonnee
type Hauteur = Coordonnee
type Plan = (Largeur, Hauteur)
data Camera = Camera Position Distance Plan

getRay :: Cooronnee -> (Camera -> Direction)
getRay (i, j) cam = let (Camera (x, y, z) _ _ _) = cam in normalize (i - x, j - y, -z)
  where
    normalize (x, y, z) = let norme = sqrt(x^2 + y^2 + z^2) in (x / norm, y / norm, z / norm)

rayTraceScene :: Scene -> Direction -> (Camera -> (Object, Distance))
rayTraceScene = -- Imaginons que l'on fait le nécessaire pour raytracer une scÚne.

computeColor :: (Object, Distance) -> Camera -> [Word8]
computeColor = -- Calcule la couleur en tenant compte de l'angle de la caméra, etc.

--Pour getPixel, on préférera souvent spécialiser d'abord la Caméra,
-- puis appeler cette spécialisation sur toute les coordonnées de l'image.
-- C'est pourquoi le type est "Camera -> Coordonnee -> [Word8]" plutĂŽt
-- que "Coordonnee -> Camera -> [Word8]". On perd donc la possibilité
-- d'utiliser getPixel avec la monade "(->) Camera".
getPixel :: Camera -> Coordonee -> [Word8])
getPixel cam coords = (return coords >>= getRay >>= (rayTraceScene uneScene) >>= computeColor) cam

Point culture

Comprenez bien que les monades ne sont pas indispensable, et que l’on faisait des monade avant la premiĂšre apparition de la classe Monade. C’est simplement de nouveaux outils Ă  votre disposition pour rĂ©soudre des problĂšmes, et ils permettent parfois de vieux problĂšmes de façon trĂšs Ă©lĂ©gante et concise (ce qui est un excellent pour l’évolutivitĂ© d’un code).

Sachez que les monades aussi doivent respecter certaines rĂšgles, tous comme les foncteurs et les foncteurs applicatifs. Cela assure qu’une monade seras bien un foncteur (applicatif), de la façon dĂ©crite plus haut. Les voici :

-- Neutre Ă  gauche
return a >>= f = f a
-- Neutre Ă  droite
m >>= return = m
-- Associativité
(m >>= f) >>= g = m >>= (\x -> f x >>= g)

Vous trouverez des liens dans les références pour plus de détails.

On n’a pas aborder la monade IO. Cette monade permet de ramener les actions propre a l’impĂ©ratif, les “effets de bord”, dans le langage haskell. L’astuce diabolique est la suivante : Puisque qu’un programme haskell ne peut produire d’effet de bord, un programme haskell dĂ©criras comment composer, jusxtaposer, et transformer les rĂ©sultats produits par des programmes impĂ©ratif. Utiliser la monade IO, c’est jouer avec la composition et la juxtaposition de programmes. C’est alors que l’opĂ©rateur >> prend tout son sens! Il permet de juxtaposer deux programmes impĂ©ratifs et ne retenir le rĂ©sultat que du second, par exemple :

afficherMessage = putStrLn "Bonjour," >> putStrLn "monde!"

En dire plus sur cette monade n’a d’intĂ©rĂȘts que pour les personne voulant Ă©crire des programmes en haskell. Si c’est votre cas, je vous invite Ă  lire, au choix, Learn You Haskell for Great Good ou (plus technique et plus 
 “professionnel”) Real World Haskell, chez O’Reilly.

Références :