如何在Haskell中解析IO String?

时间:2012-06-27 15:31:23

标签: string parsing haskell io monads

我遇到了Haskell的问题。我的文本文件看起来像这样:

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

我不知道如何获得前两个数字(上面的2和7)以及最后一行的列表。每行末尾都有点。

我尝试构建一个解析器,但是名为'readFile'的函数返回名为IO String的Monad。我不知道如何从这种类型的字符串中获取信息。

我更喜欢在一系列字符上工作。也许有一个函数可以从'IO String'转换为[Char]?

4 个答案:

答案 0 :(得分:67)

我认为你对Haskell中的IO有一个基本的误解。特别是,你这样说:

  

也许有一个函数可以从'IO String'转换为[Char]?

不,没有 1 ,而且没有这样的功能是Haskell最重要的事情之一。

Haskell是一种非常有原则的语言。它试图保持“纯”函数(没有任何副作用,并在给出相同的输入时始终返回相同的结果)和“不纯”函数(具有类似读取文件,打印等副作用)之间的区别到屏幕,写入磁盘等)。规则是:

  1. 您可以在任何地方使用纯函数(在其他纯函数中,或在不纯函数中)
  2. 您只能在其他不纯的功能中使用不纯的功能。
  3. 代码标记为纯或不纯的方式是使用类型系统。当你看到像

    这样的函数签名时
    digitToInt :: String -> Int
    

    你知道这个功能很纯粹。如果你给它String它会返回Int而且如果你给它Int ,它将总是返回相同的String 。另一方面,功能签名如

    getLine :: IO String
    

    impure ,因为String的返回类型标有IO。显然getLine(读取一行用户输入)并不总是返回相同的String,因为它取决于用户键入的内容。您不能在纯代码中使用此函数,因为添加即使最小的杂质也会污染纯代码。一旦你去IO,你永远不会回去。

    您可以将IO视为包装器。当您看到特定类型(例如x :: IO String)时,您应该将其解释为“x是一种操作,在执行时会执行某些任意I / O,然后返回{{1}类型的内容1}}“(请注意,在Haskell中,StringString完全相同。)

    那么您如何访问[Char]行动中的值?幸运的是,函数IO的类型是main(它是执行某些I / O并返回IO ()的操作,与返回任何内容相同)。因此,您始终可以使用()内的IO功能。当您执行Haskell程序时,您正在执行的是运行main函数,这会导致程序定义中的所有I / O实际执行 - 例如,您可以从文件读取和写入,请求用户输入,写入stdout等等。

    您可以考虑构建一个像这样的Haskell程序:

    • 执行I / O的所有代码都会获得main标记(基本上,您将其放在IO块中)
    • 不需要执行I / O的代码不需要在do块中 - 这些是“纯粹”功能。
    • 您的do函数将您在程序中定义的I / O操作排列在一起,使程序按照您的意愿执行(在任何您喜欢的位置穿插纯函数)。
    • 运行main时,会导致执行所有这些I / O操作。

    所以,考虑到这一切,你如何编写你的程序?好吧,功能

    main

    将文件读取为readFile :: FilePath -> IO String 。所以我们可以使用它来获取文件的内容。功能

    String

    在换行符上拆分lines:: String -> [String] ,所以现在有一个String列表,每个列表对应一行文件。功能

    String

    从列表中删除最后一个元素(这将消除每行上的最后一个init :: [a] -> [a] )。功能

    .

    获取read :: (Read a) => String -> a 并将其转换为任意Haskell数据类型,例如StringInt。合理地结合这些功能将为您提供程序。

    请注意,实际需要执行任何I / O的唯一时间是您正在读取文件。因此,这是程序中唯一需要使用Bool标记的部分。程序的其余部分可以“纯粹”编写。

    听起来你需要的是文章The IO Monad For People Who Simply Don't Care,它可以解释你的很多问题。不要被“monad”这个术语吓到 - 你不需要理解monad是什么来编写Haskell程序(注意这个段落是我答案中唯一一个使用“monad”这个词,尽管我承认我现在已经使用了四次......)


    这是(我认为)你想写的程序

    IO

    要回答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) 关于将npfedwards应用于lines输出的评论,您需要意识到readFile text.txt会为您提供readFile text.txt,而且只有IO String当您将其绑定到变量(使用contents <-)时,您可以访问基础String,以便可以将lines应用于该变量。

    请记住:一旦你去IO,你永远不会回去。


    1 我故意忽略unsafePerformIO,因为正如名称所暗示的那样,它是非常不安全的!除非你真的知道你在做什么,否则不要使用它。

答案 1 :(得分:9)

作为编程菜鸟,我也被IO混淆了。请记住,如果你去IO,你永远不会出来。克里斯写了great explanation on why。我只是想提一些如何在monad中使用IO String的例子。我将使用getLine读取用户输入并返回IO String

line <- getLine 

所有这一切都将用户输入从getLine绑定到名为line的值。如果你在ghci中输入这个,并输入:type line它将返回:

:type line
line :: String

但是等等! getLine会返回IO String

:type getLine
getLine :: IO String

那么来自IO的{​​{1}}发生了什么?发生了getLine<-是您的<-朋友。它允许您显示monad中IO污染的值,并将其与正常函数一起使用。 Monad很容易识别,因为它们以IO开头。像这样:

do

如果你像我一样,你很快就会发现main = do putStrLn "How much do you love Haskell?" amount <- getLine putStrln ("You love Haskell this much: " ++ amount) 是你最好的单身朋友,liftIO有助于减少你需要写的括号数。

那么如何从$获取信息?好吧,如果readFile的输出是readFile,那么:

IO String

然后您需要的只是友好的:type readFile readFile :: FilePath -> IO String

<-

现在,如果在ghci中键入并检查 yourdata <- readFile "samplefile.txt" 的类型,您会发现它是一个简单的yourdata

String

答案 2 :(得分:7)

正如人们已经说过的,如果你有两个函数,一个是readStringFromFile :: FilePath -> IO String,另一个是doTheRightThingWithString :: String -> Something,那么你真的不需要从IO转义一个字符串,因为您可以通过各种方式组合这两个功能:

fmap IOIOFunctor):

fmap doTheRightThingWithString readStringFromFile

(<$>) IO IOApplicative(<$>) == fmap):

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

使用liftM IOliftM == fmap):

import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile

(>>=) IO IOMonadfmap == (<$>) == liftM == \f m -> m >>= return . f):

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

使用do表示法:

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

每次你都会得到IO Something

为什么你会这样做呢?好吧,有了这个,你将拥有和 您所用语言的 referentially transparent 程序(函数)。这意味着每个类型都是IO-free的函数是 pure refereically transparent ,因此对于相同的参数,它将返回相同的值。例如,doTheRightThingWithString会为同一Something返回相同的String。但是,不是无IO的readStringFromFile每次都可以返回不同的字符串(因为文件可以更改),因此您无法从IO中删除此类不正确的值。

答案 3 :(得分:4)

如果你有这种类型的解析器:

myParser :: String -> Foo

并使用

读取文件
readFile "thisfile.txt"

然后您可以使用

读取和解析文件
fmap myParser (readFile "thisfile.txt")

结果将是IO Foo类型。

fmap表示myParser在IO内部运行。

另一种思考方式是myParser :: String -> Foofmap myParser :: IO String -> IO Foo