如何在 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
,所以现在你有一个String
s 列表,每个对应于文件的一行。功能
init :: [a] -> [a]
从列表中删除最后一个元素(这将删除每行最后的.
)。功能
read :: (Read a) => String -> a
接受String
并将其转换为任意的Haskell 数据类型,例如Int
或Bool
。合理地组合这些功能将为您提供您的程序。
请注意,您真正需要执行任何 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
的结果将是三个元素的列表,所以我利用它来调用三个元素a
、b
和c
。
“永久”名称是什么意思?除了在 do
块内之外,您还想在哪里访问变量?【参考方案2】:
作为一个编程菜鸟,我也被IO
s 搞糊涂了。请记住,如果你去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
那么来自getLine
的IO
ness 发生了什么? <-
发生了什么事。 <-
是您的 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
那么你只需要你的友好<-
:
yourdata <- readFile "samplefile.txt"
现在,如果在 ghci 中输入它并检查 yourdata
的类型,您会发现它是一个简单的 String
。
:type yourdata
text :: String
【讨论】:
amount <- getLine
是否有脱糖版本??
@matthias getLine >>= (\amount -> ...)
.【参考方案3】:
正如人们已经说过的,如果你有两个函数,一个是readStringFromFile :: FilePath -> IO String
,另一个是doTheRightThingWithString :: String -> Something
,那么你真的不需要从IO
中转义一个字符串,因为你可以将这两个函数结合起来以各种方式:
fmap
对应 IO
(IO
是 Functor
):
fmap doTheRightThingWithString readStringFromFile
(<$>)
对应 IO
(IO
是 Applicative
和 (<$>) == fmap
):
import Control.Applicative
...
doTheRightThingWithString <$> readStringFromFile
liftM
对应IO
(liftM == fmap
):
import Control.Monad
...
liftM doTheRightThingWithString readStringFromFile
(>>=)
对应 IO
(IO
是 Monad
,fmap == (<$>) == 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
。
你为什么要那样做?好吧,有了这个,您将拥有 pure 和
referentially 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 -> Foo
,fmap myParser :: IO String -> IO Foo
。
【讨论】:
以上是关于如何在 Haskell 中解析 IO 字符串?的主要内容,如果未能解决你的问题,请参考以下文章
在Haskell中使用UTF-8作为IO String读取文件