Après avoir vu comment les monades List et Maybe sont définies, je me suis naturellement intéressé à la façon dont les monades les opérations >>=
et return
sont définis pour la monade IO.
Réponses
Trop de publicités?Il n'y a pas d'implémentation spécifique pour IO
Il s'agit d'un type abstrait, dont l'implémentation exacte est laissée indéfinie par le rapport Haskell. En effet, rien n'empêche une implémentation d'implémenter IO
et son Monad
comme des primitives du compilateur, sans aucune implémentation en Haskell.
En gros, Monad
est utilisé comme un interface à IO
qui ne peut pas lui-même être implémenté en Haskell pur. C'est probablement tout ce que vous avez besoin de savoir à ce stade, et plonger dans les détails d'implémentation risque de ne faire qu'embrouiller, plutôt que de donner un aperçu.
Cela dit, si vous regardez le code source de GHC, vous constaterez qu'il représente IO a
comme une fonction ressemblant à State# RealWorld -> (# State# RealWorld, a #)
(en utilisant un tuple décomposé comme type de retour), mais cela est trompeur ; il s'agit d'un détail d'implémentation, et ces State# RealWorld
n'existent pas réellement au moment de l'exécution. IO
est pas une monade d'état, 1 en théorie ou en pratique.
Au lieu de cela, GHC utilise impur pour mettre en œuvre ces opérations d'entrée/sortie. State# RealWorld
"valeurs" ne servent qu'à empêcher le compilateur de réordonner les déclarations en introduisant des dépendances de données d'une déclaration à l'autre.
Mais si vous voulez vraiment voir l'implémentation de GHC de return
et (>>=)
les voici :
returnIO :: a -> IO a
returnIO x = IO $ \ s -> (# s, x #)
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO $ \ s -> case m s of (# new_s, a #) -> unIO (k a) new_s
où unIO
déballe simplement la fonction à partir de l'intérieur du IO
constructeur.
Il est important de noter que IO a
représente un description d'un calcul impur qui pourrait être exécuté pour produire une valeur de type a
. Le fait qu'il existe un moyen de récupérer des valeurs à partir de la représentation interne de GHC de IO
ne signifie pas que cela soit valable en général, ni que l'on puisse faire une telle chose pour toutes les monades. Il s'agit purement d'un détail d'implémentation de la part de GHC.
1 Le site monade d'état est une monade utilisée pour accéder à un état et le modifier au cours d'une série de calculs ; elle est représentée sous la forme suivante s -> (a, s)
(où s
est le type d'état), qui ressemble beaucoup au type que GHC utilise pour IO
d'où la confusion.
Vous serez déçu, mais le >>=
sur IO
La monade n'est pas très intéressante. Pour citer la source de GHC :
{- |
A value of type @'IO' a@ is a computation which, when performed,
does some I\/O before returning a value of type @a@.
There is really only one way to \"perform\" an I\/O action: bind it to
@Main.main@ in your program. When your program is run, the I\/O will
be performed. It isn't possible to perform I\/O from an arbitrary
function, unless that function is itself in the 'IO' monad and called
at some point, directly or indirectly, from @Main.main@.
'IO' is a monad, so 'IO' actions can be combined using either the do-notation
or the '>>' and '>>=' operations from the 'Monad' class.
-}
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
Cela signifie que IO
est déclarée comme l'instance de State
State#
monade, et son .>>=
y est défini (et son implémentation est assez facile à deviner)
Voir IO inside sur le wiki de Haskell pour plus de détails sur l'option IO
monade. Il est également utile de se pencher sur le Documentation sur Haskell où chaque position a un petit lien "Source" sur la droite.
Mise à jour : Et voilà une autre déception, qui est ma réponse, parce que je n'ai pas remarqué le '#' dans State#
. Cependant IO
se comporte comme State
monade porteuse abstraite RealWorld
état
Comme l'a écrit @ehird State#
est interne au compilateur et >>=
pour le IO
est définie dans GHC.IOBase module :
instance Monad IO where
{-# INLINE return #-}
{-# INLINE (>>) #-}
{-# INLINE (>>=) #-}
m >> k = m >>= \ _ -> k
return x = returnIO x
m >>= k = bindIO m k
fail s = failIO s
(...)
bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO ( \ s ->
case m s of
(# new_s, a #) -> unIO (k a) new_s
)
(...)
returnIO :: a -> IO a
returnIO x = IO (\ s -> (# s, x #))
Ils ne font rien de spécial, et sont juste là pour séquencer les actions. Il serait utile que vous leur donniez des noms différents :
>>= devient "et ensuite, en utilisant le résultat de l'action précédente,"
>> devient "et ensuite".
Le retour devient "ne rien faire, mais le résultat de ne rien faire est".
Cela transforme cette fonction :
main :: IO ()
main = putStr "hello"
>> return " world"
>>= putStrLn
devient :
main :: IO ()
main = putStr "hello" and then,
do nothing, but the result of doing nothing is " world"
and then, using the result of the previous action, putStrLn
En fin de compte, il n'y a rien de magique dans l'IO. C'est exactement aussi magique que le point-virgule l'est en C.