• Aucun résultat trouvé

Le module Control.Monad.State fournit un newtype qui enveloppe des calculs à états. Voici sa définition :

newtype State s a = State { runState :: s -> (a,s) }

Un State s a est un calcul à états qui manipule un état de type s et retourne un résultat de type a.

Maintenant qu’on a vu ce que sont des calculs à états et comment ils pouvaient être vus comme des valeurs avec des contextes, regardons leur instance de Monad :

instance Monad (State s) where

return x = State $ \s -> (x,s)

(State h) >>= f = State $ \s -> let (a, newState) = h s (State g) = f a in g newState

Regardons d’abord return. Le but de return est de prendre une valeur et d’en faire un calcul à états retournant toujours cette valeur. C’est pourquoi on crée une lambda \s -> (x, s). On présente toujours x en résultat du calcul à états et l’état est inchangé, parce que return place la valeur dans un contexte minimal. Donc return crée un calcul à états qui retourne une certaine valeur sans changer l’état.

Qu’en est-il de >>= ? Eh bien, le résultat obtenu en donnant une fonction à un calcul à états par >>= doit être un calcul à états, n’est-ce pas ? On commence donc à écrire le newtype State et une lambda. Cette lambda doit être notre nouveau calcul à états. Mais que doit-elle faire ? Eh bien, on doit extraire le résultat du premier calcul à états d’une manière ou d’une autre.

Puisqu’on se trouve dans un calcul à états, on peut donner au calcul à états h notre état actuel s, ce qui retourne une paire d’un résultat et d’un nouvel état : (a, newState). À chaque fois qu’on a implémenté >>=, après avoir extrait le résultat de la valeur monadique, on appliquait la fonction f dessus pour obtenir une nouvelle valeur monadique. Dans Writer, après avoir fait cela et

obtenu la nouvelle valeur monadique, on devait ne pas oublier de tenir compte du contexte en faisant mappend entre l’ancienne

valeur monoïdale et la nouvelle. Ici, on fait f a et on obtient un nouveau calcul à états g. Maintenant qu’on a un calcul à états et un état (qui s’appelle newState ) on applique simplement le calcul à états g à l’état newState . Le résultat est un tuple contenant le résultat final et l’état final !

Ainsi, avec >>=, on colle ensemble deux calculs à états, seulement le second est caché dans une fonction qui prend le résultat du premier. Puisque pop et push sont déjà des calculs à états, il est facile de les envelopper dans un State. Regardez :

import Control.Monad.State

pop :: State Stack Int

pop = State $ \(x:xs) -> (x,xs)

push :: Int -> State Stack ()

push a = State $ \xs -> ((),a:xs)

pop est déjà un calcul à états et push prend un Int et retourne un calcul à états. Maintenant, on peut réécrire l’exemple précédent où l’on empilait 3 sur la pile avant de dépiler deux nombres ainsi :

import Control.Monad.State

stackManip :: State Stack Int

stackManip = do

push 3 a <- pop pop

Voyez-vous comme on a collé ensemble un empilement et deux dépilements en un calcul à états ? Quand on sort ce calcul de son newtype, on obtient une fonction à laquelle on peut fournir un état initial :

ghci> runState stackManip [5,8,2,1] (5,[8,2,1])

On n’avait pas eu besoin de lier le premier pop à a vu qu’on n’utilise pas ce a. On aurait pu écrire :

stackManip :: State Stack Int

stackManip = do

push 3 pop pop

Plutôt cool. Mais et si l’on voulait faire ceci : dépiler un nombre de la pile, puis si ce nombre est 5, l’empiler à nouveau et sinon, empiler 3 et 8 plutôt ? Voici le code :

stackStuff :: State Stack ()

stackStuff = do a <- pop if a == 5 then push 5 else do push 3 push 8

C’est plutôt simple. Lançons-la sur une pile vide.

ghci> runState stackStuff [9,0,2,1,0] ((),[8,3,0,2,1,0])

Souvenez-vous, les expressions do résultent en des valeurs monadiques, et avec la monade State, une expression do est donc une fonction à états. Puisque stackManip et stackStuff sont des calculs à états ordinaires, on peut les coller ensemble pour faire des calculs plus compliqués.

moreStack :: State Stack ()

moreStack = do

a <- stackManip if a == 100

then stackStuff else return ()

Si le résultat de stackManip sur la pile actuelle est 100, on fait stackStuff, sinon on ne fait rien. return () conserve l’état comme il est et ne fait rien. Le module Control.Monad.State fournit une classe de types appelée MonadState qui contient deux fonctions assez utiles, j’ai nommé get et put. Pour

State , la fonction get est implémentée ainsi :

get = State $ \s -> (s,s)

Elle prend simplement l’état courant et le présente en résultat. La fonction put prend un état et crée une fonction à états qui remplace l’état courant par celui-ci :

put newState = State $ \s -> ((),newState)

Avec ces deux fonctions, on peut voir la pile courante ou la remplacer par une toute nouvelle pile. Comme ça :

stackyStack :: State Stack ()

stackyStack = do

stackNow <- get

if stackNow == [1,2,3] then put [8,3,1] else put [9,2,1]

Il est intéressant d’examiner le type qu’aurait >>= si elle était restreinte aux valeurs State : (>>=) :: State s a -> (a -> State s b) -> State s b

Remarquez que le type de l’état s reste le même, mais le type du résultat peut changer de a en b. Cela signifie que l’on peut coller ensemble des calculs à états dont les résultats sont de différents types, mais le type des états doit être le même. Pourquoi cela ? Eh bien, par exemple, pour Maybe, >>= a ce type :

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

Il est logique que la monade elle-même, Maybe, ne change pas. Ça n’aurait aucun sens d’utiliser >>= entre deux monades différentes. Eh bien, pour la monade d’états, la monade est en fait State s, donc si ce s était différent, on utiliserait >>= entre deux monades différentes.