Une valeur de type StateT s m a
, modulo newtype
est une fonction de type s -> m (a, s)
.
La fonction (return . (\ s -> (s, s)))
a le type s -> m (s, s)
Ainsi, une fois qu'elle est enveloppée par le StateT
devient une valeur de type StateT s m s
.
Notez qu'une valeur de type StateT s m (s, s)
impliquerait plutôt une fonction de type s -> m (s, (s, s))
ce qui n'est pas le cas ici.
Votre confusion semble provenir de l'"autre" s
en m (s, s)
qui ne contribue pas à la x
lorsque nous courons x <- get
. Pour comprendre pourquoi, il est utile de réfléchir à ce qu'un calcul avec état effectue :
- Tout d'abord, nous lisons l'ancien état du type
s
. Il s'agit de la s -> ..
dans le type s -> m (a, s)
.
- Ensuite, nous exécutons une action dans la monade
m
. Il s'agit de la .. -> m ..
dans le type s -> m (a, s)
.
- L'action monadique renvoie un nouvel état qui remplace l'ancien. Il s'agit de l'action
.. -> .. (.., s)
dans le type s -> m (a, s)
.
- Enfin, l'action monadique renvoie également une valeur, d'un type éventuellement différent
a
. Ce projet .. -> .. (a, ..)
dans le type s -> m (a, s)
.
La course à pied x <- action
gère automatiquement tous ces types de données pour nous, et permet à la x
pour avoir le type de résultat a
, uniquement.
Concrètement, considérons ce pseudo-code impératif :
global n: int
def foo():
if n > 5:
print ">5"
n = 8
return "hello"
else:
print "not >5"
n = 10
return "greetings"
Dans un langage impératif, nous taperions ceci sous la forme suivante foo(): string
puisqu'elle renvoie une chaîne de caractères, sans tenir compte de ses effets secondaires sur le système global de gestion de l'information. n: int
et les messages imprimés.
En Haskell, nous modéliserions plutôt cela en utilisant un type plus précis comme
Int -> IO (String, Int)
^-- the old n
^-- the printed stuff
^-- the returned string
^-- the new n
Une fois de plus, l'exécution x <- foo()
nous voulons x: string
, pas x: (string, int)
, à l'instar de ce qui se passerait dans un langage impératif.
Si, à la place, nous avions une fonction
global n: int
def bar():
old_n = n
n = n + 5
return old_n
nous utiliserions le type
Int -> IO (Int, Int)
puisque la valeur retournée est un Int
maintenant. De même,
global n: int
def get():
return n
pourrait utiliser le même type
Int -> IO (Int, Int)
On pourrait arguer que la deuxième Int
n'est pas strictement nécessaire ici, puisque get()
n'est pas vraiment en train de produire un nouvel état - ce n'est pas changeant la valeur de n
. Cependant, il est pratique d'utiliser un type de la même forme, s -> m (a, s)
comme toute fonction qui podría changer l'état. Cela permet de l'utiliser avec n'importe quelle autre fonction de même type.