Je ne suis pas sûr que l'intuition que vous avez développée (telle qu'elle est décrite dans votre auto-écriture) vous permette d'atteindre vos objectifs. répondre à cette question) est tout à fait exacte. Permettez-moi d'essayer de vous donner une meilleure intuition. Vous trouverez peut-être aussi cette vieille réponse qui est la mienne utile, bien que la question soit tout à fait différente.
En Haskell, une valeur de type IO a
(pour tout type a
, de sorte qu'un IO Int
ou un IO String
ou autre) est parfois appelée "action IO", mais comme l'a mentionné @WillNess dans un commentaire, il est préférable de la considérer comme une "recette IO". Pour ces recettes, nous considérons que l'"évaluation" et l'"exécution" sont des opérations complètement distinctes. Évaluer une expression de type IO a
c'est comme écrire la recette. Le résultat de évaluant une expression de type IO Int
est une valeur de type IO Int
. La production de cette valeur n'effectue aucune E/S et ne prend pas de temps à proprement parler, même si les E/S sous-jacentes impliquent des retards ou d'autres opérations lentes. Cette valeur évaluée IO a
peut être transmise, stockée, dupliquée, modifiée, combinée à d'autres valeurs. IO
ou complètement ignorées, le tout sans effectuer d'entrées/sorties réelles.
En revanche, Exécution la recette qui en résulte est le processus d'exécution des opérations d'E/S. Le résultat de la Exécution un IO Int
est un Int
. S'il faut 20 minutes de retard, d'accès aux fichiers et/ou de réquisition par pigeon voyageur pour obtenir cette Int
L'opération prendra un certain temps. Si vous exécuter la même recette deux fois, cela n'ira pas plus vite la deuxième fois.
La quasi-totalité du code que nous écrivons en Haskell évalue Recettes IO sans les exécuter.
Lorsque le code :
slowOne = do
threadDelay (10 ^ 6)
return 1
est exécuté, il ne fait qu'évaluer (écrire) une recette IO. L'évaluation de cette recette implique évidemment l'évaluation d'un do-block. Cela n'implique pas do l'E/S ; il se contente d'évaluer (écrire) chacune des recettes du bloc-do et de les combiner en une recette écrite plus importante.
Plus précisément, l'évaluation slowOne
impliquent :
-
Évaluation de la recette threadDelay (10 ^ 6)
. Il s'agit d'évaluer l'expression arithmétique 10 ^ 6
et appeler la fonction threadDelay
sur celui-ci. Cette fonction est implémentée (pour l'exécution non threadée) comme suit :
threadDelay :: Int -> IO ()
threadDelay time = IO $ \s -> some_function_of_s
C'est-à-dire qu'il enveloppe une fonction dans un fichier IO
pour produire une valeur de type IO ()
. Il est important de noter que cela ne retarde pas le fil de discussion. Il crée simplement une valeur fonctionnelle (enveloppée). Il n'y a rien de magique dans la méthode IO
d'ailleurs. Il s'agit d'un threadDelay
est similaire à l'écriture d'un texte tout aussi non magique :
justAFunction :: Int -> Maybe (Int -> Int)
justAFunction c = Just (\x -> c*x)
-
Évaluation de la recette return 1
. Cela aussi crée simplement une valeur enveloppée dans un fichier IO
constructeur. Plus précisément, il s'agit de la valeur fonctionnelle (enveloppée et complètement non magique) qui ressemble à quelque chose comme :
IO (\s -> (s, 1))
-
Combiner ces deux recettes évaluées séquentiellement en une recette plus longue. Cette nouvelle recette combinée est la valeur de type IO Int
qui sera attribuée à slowOne
.
De même, lorsque le code suivant est évalué :
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
vous n'exécutez aucune IO. Vous évaluez simplement les recettes d'IO et les structures de données qui contiennent des recettes d'IO. Plus précisément, vous évaluez cette expression à une valeur de type [IO Integer]
consistant en une liste infinie de IO Integer
valeurs/recettes. La première recette de la liste est slowOne
. La deuxième recette de la liste est la suivante :
fmap (+1) slowOne
Ceci nécessite un mot d'explication. Lorsque cette expression est évaluée, elle construit une nouvelle recette qui pourrait être écrite en utilisant un do-block équivalent :
fmap_plus_one_of_slowOne = do
x <- slowOne
return (x + 1)
Compte tenu de l'importance de la slowOne
est défini, cela équivaut en fait à la recette autonome que nous obtenons en évaluant :
fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 2
De même, la troisième recette de la liste :
fmap (+1) (fmap (+1) slowOne)
évalue l'équivalent de la recette :
fmap_plus_one_of_fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 3
La dernière partie de votre programme est la suivante :
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
Vous serez peut-être surpris d'apprendre que, lors de l'évaluation de ce code, nous sommes encore sólo évaluant et non Exécution recettes. Lorsque cette mapM_
est évaluée, elle construit une nouvelle recette. La recette qu'elle construit pourrait être décrite avec des mots comme :
"Prenez chaque recette de la liste infiniteInts
. Désolé pour le mauvais choix de nom -- il ne s'agit pas d'une liste d'entiers, mais d'une liste d'IO recettes pour obtenir des nombres entiers. Heureusement que vous êtes un ordinateur et que vous ne vous y perdrez pas, hein ? Quoi qu'il en soit, prenez chacune de ces recettes dans l'ordre et passez-les à la fonction que j'ai ici pour générer une nouvelle recette. Ensuite, exécutez cette liste de recettes dans l'ordre. Vous êtes en train de l'écrire, n'est-ce pas ? Arrêtez, ne exécuter rien encore ! Il suffit de l'écrire !"
Récapitulons :
-
slowOne
est la recette
do threadDelay (10 ^ 6)
return 1
-
fmap (+1) slowOne
est identique à la recette :
do threadDelay (10 ^ 6)
return 2
-
de la même manière, fmap (+1) (fmap (+1) slowOne)
n'est en fait qu'une recette
do threadDelay (10 ^ 6)
return 3
et ainsi de suite
-
donc, infiniteInts
est la liste des recettes :
infiniteInts =
[ do { threadDelay (10 ^ 6); return 1 }
, do { threadDelay (10 ^ 6); return 2 }
, do { threadDelay (10 ^ 6); return 3 }
, ... ]
Compte tenu de la signification de la mapM_ ...
recette, si Haskell permettait des programmes infiniment longs, nous aurions pu écrire toute cette recette à partir de zéro comme suit :
do -- first recipe
threadDelay (10 ^ 6)
i <- return 1
print i
-- second recipe
threadDelay (10 ^ 6)
i <- return 2
print i
-- third recipe
threadDelay (10 ^ 6)
i <- return 3
print i
-- etc.
C'est la recette qui résulte de l'évaluation de la mapM_ ...
l'expression.
Enfin, nous arrivons à la seulement une partie de votre programme qui exécute une recette IO au lieu de simplement l'évaluer. Cette partie est :
main = ...
Lorsque vous nommez une recette main
vous demandez à Haskell de l'exécuter lors de l'exécution du programme. Comme vous pouvez le voir dans la valeur évaluée de la recette que vous avez assignée à main
Il s'agit d'une recette combinée qui implique l'intercalation de threadDelay
y print
Ainsi, lorsqu'elle est exécutée, elle imprime une liste croissante d'entiers avec des délais avant chaque entier.
Un mot sur la paresse et la rigueur... La paresse ne joue aucun rôle dans le processus ci-dessus (sauf pour nous permettre de construire une liste infinie sans bloquer la machine). Lorsque je dis "évaluer" ci-dessus, cela ne fait aucune différence que l'évaluation soit stricte et se produise immédiatement ou que l'évaluation soit techniquement retardée jusqu'à ce qu'elle soit nécessaire. Le moment où elle est nécessaire pourrait Mais l'évaluation (rédaction de la recette) et l'exécution (suivi de la recette) restent des processus distincts, même s'ils se déroulent l'un après l'autre.