如何在 Haskell 中解析 IO 字符串?

Posted

技术标签:

【中文标题】如何在 Haskell 中解析 IO 字符串?【英文标题】:How can I parse the IO String in Haskell? 【发布时间】:2012-06-29 02:24:23 【问题描述】:

我在使用 Haskell 时遇到了问题。我的文本文件如下所示:

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

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

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

我更喜欢处理一系列字符。也许有一个函数可以将'IO String'转换为[Char]?

【问题讨论】:

请注意,String 只是 [Char] 的类型别名,所以你和 [Char] 之间唯一的东西就是 IO(你无法摆脱它 - 你需要在里面工作IO monad(有关更多信息,请参阅任何 monad 教程)。另请注意,[Char] 是字符的(链接)列表,而不是数组。 【参考方案1】:

我认为您对 Haskell 中的 IO 存在根本性的误解。特别是,你这样说:

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

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

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

    您可以在任何地方使用纯函数(在其他纯函数中,或在不纯函数中) 您只能在其他不纯函数中使用不纯函数。

代码被标记为纯或不纯的方式是使用类型系统。当你看到像

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

你知道这个函数是纯粹的。如果你给它一个String,它会返回一个Int,而且如果你给它同样的String,它总是会返回同样的Int。另一方面,像

这样的函数签名
getLine :: IO String

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

您可以将IO 视为一个包装器。当您看到特定类型时,例如 x :: IO String,您应该将其解释为“x 是一个操作,当执行时,它会执行一些任意 I/O,然后返回 String 类型的内容”(注意在 Haskell 中,String[Char] 完全一样)。

那么,您如何访问来自IO 操作的值?幸运的是,函数main 的类型是IO ()(这是一个执行一些I/O 并返回() 的操作,这与什么都不返回相同)。所以你总是可以在main 中使用你的IO 函数。当你执行一个 Haskell 程序时,你正在做的是运行 main 函数,这会导致程序定义中的所有 I/O 都被实际执行——例如,你可以从文件中读取和写入,询问用户输入,写入标准输出等。

你可以考虑这样构建一个 Haskell 程序:

所有执行 I/O 的代码都会获得 IO 标签(基本上,你将它放在 do 块中) 不需要执行 I/O 的代码不需要位于 do 块中 - 这些是“纯”函数。 您的 main 函数将您定义的 I/O 操作按顺序排列在一起,以使程序执行您希望它执行的操作(在您喜欢的任何地方穿插纯函数)。 当您运行 main 时,您会执行所有这些 I/O 操作。

那么,考虑到所有这些,您如何编写程序?嗯,函数

readFile :: FilePath -> IO String

将文件读取为String。所以我们可以使用它来获取文件的内容。功能

lines:: String -> [String]

在换行符上拆分String,所以现在你有一个Strings 列表,每个对应于文件的一行。功能

init :: [a] -> [a]

从列表中删除最后一个元素(这将删除每行最后的.)。功能

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

接受String 并将其转换为任意的Haskell 数据类型,例如IntBool。合理地组合这些功能将为您提供您的程序。

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

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


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

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)

要回答关于将lines 应用于readFile text.txt 的输出的npfedwards 评论,您需要意识到readFile text.txt 为您提供IO String,并且仅当您将其绑定到变量时(使用@ 987654369@),您可以访问底层String,以便您可以将lines应用于它。

记住:一旦你去了IO,你就再也回不去了。


1 我故意忽略unsafePerformIO,因为顾名思义,它非常不安全!除非您真的知道自己在做什么,否则永远不要使用它。

【讨论】:

我觉得我们甚至不应该在这些答案中提到 不应命名的函数,如果只是因为 Haskell 的一些学生可能会看到它,请看类型定义,然后简单地说“啊哈!这就是我一直以来的样子! 不是说即使你把 IO monad 中的纯函数和函数小心翼翼地分开,编译器还是会以某种方式内联它? 尝试将lines 应用于readFile 的输出时出现类型错误 @npfedwards 酷。请记住,let 只是暂时给事物命名的一种方式。我做的唯一复杂的事情是在let [a,b,c] = lines contents 行中。在这里我知道lines contents 的结果将是三个元素的列表,所以我利用它来调用三个元素abc “永久”名称是什么意思?除了在 do 块内之外,您还想在哪里访问变量?【参考方案2】:

作为一个编程菜鸟,我也被IOs 搞糊涂了。请记住,如果你去IO,你永远不会出来。克里斯写了一个great explanation on why。我只是认为给出一些关于如何在 monad 中使用 IO String 的示例可能会有所帮助。我将使用getLine 读取用户输入并返回IO String

line <- getLine 

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

:type line
line :: String

但是等等! getLine 返回 IO String

:type getLine
getLine :: IO String

那么来自getLineIOness 发生了什么? &lt;- 发生了什么事。 &lt;- 是您的 IO 朋友。它允许您在 monad 中取出被 IO 污染的值,并将其与您的正常功能一起使用。 Monad 很容易识别,因为它们以 do 开头。像这样:

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

如果你像我一样,你很快就会发现 liftIO 是你的下一个最好的 monad 朋友,而 $ 有助于减少你需要编写的括号数量。

那么您如何从readFile 获取信息?那么如果readFile 的输出是IO String 就像这样:

:type readFile
readFile :: FilePath -> IO String

那么你只需要你的友好&lt;-

 yourdata <- readFile "samplefile.txt"

现在,如果在 ghci 中输入它并检查 yourdata 的类型,您会发现它是一个简单的 String

:type yourdata
text :: String

【讨论】:

amount &lt;- getLine 是否有脱糖版本?? @matthias getLine &gt;&gt;= (\amount -&gt; ...).【参考方案3】:

正如人们已经说过的,如果你有两个函数,一个是readStringFromFile :: FilePath -&gt; IO String,另一个是doTheRightThingWithString :: String -&gt; Something,那么你真的不需要从IO中转义一个字符串,因为你可以将这两个函数结合起来以各种方式:

fmap 对应 IOIOFunctor):

fmap doTheRightThingWithString readStringFromFile

(&lt;$&gt;) 对应 IOIOApplicative(&lt;$&gt;) == fmap):

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

liftM 对应IO (liftM == fmap):

import Control.Monad

...

liftM doTheRightThingWithString readStringFromFile

(&gt;&gt;=) 对应 IOIOMonadfmap == (&lt;$&gt;) == liftM == \f m -&gt; m &gt;&gt;= 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

你为什么要那样做?好吧,有了这个,您将拥有 purereferentially transparent 用您的语言编写的程序(函数)。这意味着每个类型为 IO-free 的函数都是并且引用透明,因此对于相同的参数,它将返回相同的值。例如,doTheRightThingWithString 将为相同的String 返回相同的Something。但是readStringFromFile不是无IO的,每次都可以返回不同的字符串(因为文件可以改变),所以你不能从IO中转义这种不纯的值。

【讨论】:

【参考方案4】:

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

myParser :: String -> Foo

然后您使用

读取文件
readFile "thisfile.txt"

然后您可以使用读取和解析文件

fmap myParser (readFile "thisfile.txt")

结果的类型为IO Foo

fmap 表示 myParser 在 IO“内部”运行。

另一种思考方式是myParser :: String -&gt; Foofmap myParser :: IO String -&gt; IO Foo

【讨论】:

以上是关于如何在 Haskell 中解析 IO 字符串?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell IO(字符串)和字符串

在Haskell中使用UTF-8作为IO String读取文件

基于 Haskell 中的字符串映射证明打印函数的穷举性

Haskell:FRP 反应性 Parsec?

Haskell:在 ByteStrings 和不同的文本编码之间进行转换

在Haskell中,如何创建具有多种类型的列表?