3 votes

Liste avec la première valeur de l'IO

J'ai créé une liste infinie dont le premier élément met un peu de temps à être généré :

slowOne = do
  threadDelay (10 ^ 6)
  return 1

infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
  loop :: IO Integer -> [IO Integer]
  loop ioInt = ioInt : loop (fmap (+1) ioInt)

Lorsque j'imprime la liste, je peux observer que le retard se produit non seulement au niveau du premier élément, mais aussi au niveau des éléments suivants tous les éléments :

main =
  mapM_
      (\ioInt -> do
        i <- ioInt
        print i
      )
    infiniteInts

J'essaie d'améliorer mon intuition en matière d'OI : Pourquoi y a-t-il un délai pour chaque élément, et pas seulement pour le premier qui est généré avec slowOne ?

2voto

K. A. Buhr Points 14622

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 :

  1. É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)
  2. É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))
  3. 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.

0voto

Matthias Braun Points 1114

Avertissement :

Cette réponse est incorrecte en ce qui concerne les expressions courantes. Se référer à K. Réponse de K. A. Buhr pour mieux comprendre le fonctionnement de cette liste de valeurs d'OI.


Nous pouvons comprendre ce comportement en

Voici les réécritures de infiniteInts pour obtenir différents nombres d'éléments :

Obtenir un élément

slowOne : loop (fmap (+1) slowOne)

Étant donné que la méthode Haskell : n'est pas stricte, nous exécutons slowOne une fois que cela prend une seconde.

Obtenir deux éléments

slowOne : (fmap (+1) slowOne) : loop (fmap (+1) (fmap (+1) slowOne))

En prenant deux éléments (jusqu'au deuxième : ) provoque slowOne doit être appelé deux fois, ce qui prend deux secondes.

Obtenir trois éléments

slowOne : (fmap (+1) slowOne) : (fmap (+1) (fmap (+1) slowOne)) : loop (fmap (+1) (fmap (+1) (fmap (+1) slowOne)))

En prenant trois éléments (jusqu'au troisième : ) provoque slowOne doit être appelé trois fois, ce qui prend trois secondes.

Résumé

Les réécritures nous permettent de constater que slowOne est appelé pour chaque élément (par exemple, trois fois pour trois éléments) et étant donné que GHC ne met pas en cache les résultats créés à l'intérieur d'IO (ce qui est le cas de slowOne ), il s'ensuit que chaque élément prend une seconde pour être créé.

0voto

MikaelF Points 1228

Tous les chiffres sont retardés parce qu'ils proviennent tous de slowOne . Considérez votre loop :

loop ioInt = ioInt : loop (fmap (+1) ioInt)
     ^----This is slowOne              ^
                                       ----This is also slowOne

Ma propre intuition sur ce qu'est votre fmap agit simplement sur la valeur de l'IO (l'élément Integer en IO Integer ), mais en conservant tout son contexte (la partie "IO").

Comme l'a commenté Will Ness :

fmap (1+) prend une valeur IO (une valeur pure de type IO t pour un certain t) qui décrit l'action I/O qui renverra une valeur pure x :: t et crée une nouvelle valeur pure d'E/S décrivant une action d'E/S augmentée qui renverra la valeur pure x+1 après avoir effectué les actions d'E/S décrites par la première valeur d'E/S.

À titre d'exemple, nous pourrions remplacer slowOne par une autre fonction timedOne :

timedOne = do
  time <- getPOSIXTime
  putStrLn $ "time: " ++ show time
  return 1

Appel loop con timedOne au lieu de slowOne l'imprimerait, montrant ainsi comment fmap affecte la valeur sans affecter le contexte :

time: 1583715559.051068s
1
time: 1583715559.051705s
2
time: 1583715559.052311s
3
... and so on

Vous voyez que chaque numéro utilisé porte toujours son propre "bagage" IO, sauf que cette fois-ci, ce bagage est "récupérer l'heure de l'horloge du système et l'imprimer". Si vous voulez changer ce comportement pour que seul le premier numéro soit retardé, vous devez purger la queue de la liste construite par loop de tout bagage IO retardant les threads. Une façon de le faire est d'utiliser une liste pure et d'envelopper chaque élément dans IO :

loop ioInt = ioInt : (return <$> [2..])

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X