web-dev-qa-db-fra.com

Comment puis-je analyser la chaîne IO dans Haskell?

J'ai un problème avec Haskell. J'ai un fichier texte ressemblant à ceci:

5.
7. 
[(1,2,3),(4,5,6),(7,8,9),(10,11,12)].

Je ne sais pas comment puis-je obtenir les 2 premiers numéros (2 et 7 ci-dessus) et la liste de la dernière ligne. Il y a des points à la fin de chaque ligne.

J'ai essayé de construire un analyseur, mais la fonction appelée 'readFile' renvoie la Monade appelée IO String. Je ne sais pas comment puis-je obtenir des informations de ce type de chaîne.

Je préfère travailler sur un tableau de caractères. Peut-être y a-t-il une fonction qui peut convertir de 'IO String' en [Char]?

28
Simon

Je pense que vous avez un malentendu fondamental à propos de IO dans Haskell. En particulier, vous dites ceci:

Peut-être existe-t-il une fonction qui peut convertir de 'IO String' en [Char]?

Non, il n'y en a pas1, et le fait qu'il n'y ait pas une telle fonction est l'une des choses les plus importantes chez Haskell.

Haskell est une langue très fondée sur des principes. Il essaie de maintenir une distinction entre les fonctions "pures" (qui n'ont pas d'effets secondaires et retournent toujours le même résultat lorsqu'elles donnent la même entrée) et les fonctions "impures" (qui ont des effets secondaires comme la lecture de fichiers, l'impression à l'écran, écriture sur le disque, etc.). Les règles sont les suivantes:

  1. Vous pouvez utiliser une fonction pure n'importe où (dans d'autres fonctions pures ou dans des fonctions impures)
  2. Vous ne pouvez utiliser des fonctions impures qu'à l'intérieur d'autres fonctions impures.

La façon dont ce code est marqué comme pur ou impur utilise le système de type. Lorsque vous voyez une signature de fonction comme

digitToInt :: String -> Int

vous savez que cette fonction est pure. Si vous lui donnez un String, il retournera un Int et en plus il retournera toujours le même Int si vous lui donnez le même String. D'un autre côté, une signature de fonction comme

getLine :: IO String

est impur , car le type de retour de String est marqué avec IO. De toute évidence, getLine (qui lit une ligne d'entrée utilisateur) ne retournera pas toujours le même String, car cela dépend de ce que l'utilisateur tape. Vous ne pouvez pas utiliser cette fonction en code pur, car l'ajout, même le plus petit bit d'impureté, polluera le code pur. Une fois que vous êtes allé IO, vous ne pouvez plus revenir en arrière.

Vous pouvez considérer IO comme un wrapper. Lorsque vous voyez un type particulier, par exemple, x :: IO String, Vous devez interpréter cela comme signifiant que "x est une action qui, lorsqu'elle est exécutée, effectue des E/S arbitraires, puis renvoie quelque chose de type String "(notez que dans Haskell, String et [Char] Sont exactement la même chose).

Alors, comment pouvez-vous accéder aux valeurs d'une action IO? Heureusement, le type de la fonction main est IO () (c'est une action qui fait des E/S et retourne (), Ce qui revient à ne rien retourner). Vous pouvez donc toujours utiliser vos fonctions IO dans main. Lorsque vous exécutez un programme Haskell, vous exécutez la fonction main, ce qui entraîne l'exécution de toutes les E/S de la définition de programme - par exemple, vous pouvez lire et écrire à partir de fichiers, demandez l'utilisateur pour entrée, écrire sur stdout, etc., etc.

Vous pouvez penser à structurer un programme Haskell comme ceci:

  • Tout le code qui effectue des E/S obtient la balise IO (en gros, vous le mettez dans un bloc do)
  • Le code qui n'a pas besoin d'effectuer d'E/S n'a pas besoin d'être dans un bloc do - ce sont les fonctions "pures".
  • Votre fonction main enchaîne les actions d'E/S que vous avez définies dans un ordre qui fait que le programme fait ce que vous voulez qu'il soit (entrecoupé des fonctions pures où vous le souhaitez).
  • Lorsque vous exécutez main, vous provoquez l'exécution de toutes ces actions d'E/S.

Donc, compte tenu de tout cela, comment écrivez-vous votre programme? Eh bien, la fonction

readFile :: FilePath -> IO String

lit un fichier en tant que String. Nous pouvons donc l'utiliser pour obtenir le contenu du fichier. La fonction

lines:: String -> [String]

divise un String sur les sauts de ligne, vous avez donc maintenant une liste de String, chacune correspondant à une ligne du fichier. La fonction

init :: [a] -> [a]

Supprime le dernier élément d'une liste (cela supprimera le . Final sur chaque ligne). La fonction

read :: (Read a) => String -> a

prend un String et le transforme en un type de données Haskell arbitraire, tel que Int ou Bool. La combinaison judicieuse de ces fonctions vous donnera votre programme.

Notez que la seule fois où vous devez réellement effectuer des E/S est lorsque vous lisez le fichier. C'est donc la seule partie du programme qui doit utiliser la balise IO. Le reste du programme peut être écrit "purement".

On dirait que ce dont vous avez besoin est l'article La IO Monade pour les gens qui s'en fichent , qui devrait expliquer beaucoup de vos questions. Ne soyez pas effrayé par le terme "monade" - vous n'avez pas besoin de comprendre ce qu'est une monade pour écrire des programmes Haskell (notez que ce paragraphe est le seul dans ma réponse qui utilise le mot "monade", même si je l'ai utilisé quatre fois maintenant ...)


Voici le programme que (je pense) vous voulez écrire

run :: IO (Int, Int, [(Int,Int,Int)])
run = do
  contents <- readFile "text.txt"   -- use '<-' here so that 'contents' is a String
  let [a,b,c] = lines contents      -- split on newlines
  let firstLine  = read (init a)    -- 'init' drops the trailing period
  let secondLine = read (init b)    
  let thirdLine  = read (init c)    -- this reads a list of Int-tuples
  return (firstLine, secondLine, thirdLine)

Pour répondre au commentaire de npfedwards sur l'application de lines à la sortie de readFile text.txt, Vous devez vous rendre compte que readFile text.txt Vous donne un IO String, Et ce n'est que lorsque vous le liez à une variable (en utilisant contents <-) que vous avez accès au String sous-jacent, de sorte que vous pouvez lui appliquer lines.

Rappelez-vous: une fois que vous partez IO, vous n'y retournez jamais.


1 J'ignore délibérément unsafePerformIO parce que, comme l'indique le nom, c'est très dangereux! Ne l'utilisez jamais à moins que vous ne sachiez vraiment ce que vous faites.

69
Chris Taylor

En tant que noob de programmation, moi aussi j'étais confus par IOs. N'oubliez pas que si vous allez IO vous ne sortez jamais. Chris a écrit un grande explication sur pourquoi . Je pensais juste que cela pourrait aider à donner quelques exemples sur la façon d'utiliser IO String dans une monade. Je vais utiliser getLine qui lit les entrées utilisateur et renvoie un IO String.

line <- getLine 

Tout cela ne fait que lier l'entrée utilisateur de getLine à une valeur nommée line. Si vous saisissez ceci dans ghci et saisissez :type line il renverra:

:type line
line :: String

Mais attendez! getLine renvoie un IO String

:type getLine
getLine :: IO String

Alors, qu'est-il arrivé à la IOness de getLine? <- c'est ce qui s'est passé. <- est votre IO ami. Il vous permet de faire ressortir la valeur contaminée par le IO dans une monade et de l'utiliser avec vos fonctions normales. Les monades sont facilement identifiables car elles commencent par do. Ainsi:

main = do
    putStrLn "How much do you love Haskell?"
    amount <- getLine
    putStrln ("You love Haskell this much: " ++ amount) 

Si vous êtes comme moi, vous découvrirez bientôt que liftIO est votre prochain meilleur ami monade, et que $ aide à réduire le nombre de parenthèses à écrire.

Alors, comment obtenez-vous les informations de readFile? Eh bien, si la sortie de readFile est IO String ainsi:

:type readFile
readFile :: FilePath -> IO String

Ensuite, tout ce dont vous avez besoin est votre sympathique <-:

 yourdata <- readFile "samplefile.txt"

Maintenant, si vous tapez cela dans ghci et vérifiez le type de yourdata, vous remarquerez que c'est un simple String.

:type yourdata
text :: String
9
pooya72

Comme les gens le disent déjà, si vous avez deux fonctions, l'une est readStringFromFile :: FilePath -> IO String, Et l'autre est doTheRightThingWithString :: String -> Something, Alors vous n'avez pas vraiment besoin d'échapper une chaîne de IO, puisque vous pouvez combiner ces deux fonctions de différentes manières:

Avec fmap pour IO (IO est Functor):

fmap doTheRightThingWithString readStringFromFile

Avec (<$>) Pour IO (IO est Applicative et (<$>) == fmap):

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

Avec liftM pour IO (liftM == fmap):

import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile

Avec (>>=) Pour IO (IO est Monad, fmap == (<$>) == liftM == \f m -> m >>= return . f):

readStringFromFile >>= \string -> return (doTheRightThingWithString string)
readStringFromFile >>= \string -> return $ doTheRightThingWithString string
readStringFromFile >>= return . doTheRightThingWithString
return . doTheRightThingWithString =<< readStringFromFile

Avec la notation do:

do
  ...
  string <- readStringFromFile
  -- ^ you escape String from IO but only inside this do-block
  let result = doTheRightThingWithString string
  ...
  return result

Chaque fois, vous obtiendrez IO Something.

Pourquoi voudriez-vous le faire comme ça? Eh bien, avec cela, vous aurez pur et référentiellement transparent programmes (fonctions) dans votre langue. Cela signifie que chaque fonction dont le type est sans E/S est pure et référentiellement transparente , de sorte que pour les mêmes arguments, il renvoie les mêmes valeurs. Par exemple, doTheRightThingWithString renverrait le même Something pour le même String. Cependant readStringFromFile qui n'est pas sans E/S peut renvoyer des chaînes différentes à chaque fois (car le fichier peut changer), de sorte que vous ne pouvez pas échapper une telle valeur impure de IO.

8
JJJ

Si vous avez un analyseur de ce type:

myParser :: String -> Foo

et vous lisez le fichier en utilisant

readFile "thisfile.txt"

alors vous pouvez lire et analyser le fichier en utilisant

fmap myParser (readFile "thisfile.txt")

Le résultat de cela aura le type IO Foo.

fmap signifie myParser s'exécute "à l'intérieur" de l'IO.

Une autre façon de penser est que tandis que myParser :: String -> Foo, fmap myParser :: IO String -> IO Foo.

5
dave4420