我遇到了 Haskell 的问题。我的文本文件如下所示:
5.
7.
[(1,2,3),(4,5,6),(7,8,9),(10,11,12)].
我不知道如何获得前 2 个数字(上面的 2 和 7)和最后一行的列表。每行的末尾都有点。
我试图构建一个解析器,但名为“readFile”的函数返回名为 IO 字符串的 Monad。我不知道如何从那种类型的字符串中获取信息。
我更喜欢处理一系列字符。也许有一个函数可以从 'IO String' 转换为 [Char]?
我认为您对 Haskell 中的 IO 有一个基本的误解。特别是,你这样说:
也许有一个函数可以从 'IO String' 转换为 [Char]?
不,没有1,而且没有这样的功能是 Haskell 最重要的事情之一。
Haskell 是一种非常有原则的语言。它试图区分“纯”函数(没有任何副作用,并且在给出相同输入时总是返回相同的结果)和“不纯”函数(具有从文件读取、打印等副作用)到屏幕,写入磁盘等)。规则是:
代码被标记为纯或不纯的方式是使用类型系统。当您看到类似的函数签名时
digitToInt :: String -> Int
你知道这个函数是纯粹的。如果你给它 a String
,它会返回 an Int
,而且如果你给它,它总是会返回Int
相同的String
。另一方面,函数签名如
getLine :: IO String
是不纯的,因为 的返回类型用String
标记IO
。显然getLine
(读取一行用户输入)不会总是返回相同的String
,因为它取决于用户输入的内容。你不能在纯代码中使用这个函数,因为即使添加最少量的杂质也会污染纯代码代码。一旦你走了IO
,你就再也回不去了。
您可以将其IO
视为包装器。例如,当您看到特定类型时x :: IO String
,您应该将其解释为“x
在执行时执行一些任意 I/O 然后返回某种类型的操作String
”(请注意,在 Haskell 中,String
两者[Char]
完全相同事物)。
那么你如何从一个IO
动作中获取值呢?幸运的是,函数的类型main
是IO ()
(它是一个执行一些 I/O 并返回的操作()
,这与什么都不返回相同)。所以你总是可以在IO
里面使用你的函数main
。当你执行一个 Haskell 程序时,你正在做的是运行main
函数,这会导致程序定义中的所有 I/O 都被实际执行——例如,你可以从文件中读取和写入,询问用户输入,写入到标准输出等
你可以考虑像这样构建一个 Haskell 程序:
IO
标签(基本上,你把它放在一个do
块中)do
块中——这些是“纯”函数。main
函数将您定义的 I/O 操作按顺序排列在一起,以使程序执行您希望它执行的操作(在您喜欢的任何地方穿插纯函数)。main
,您会执行所有这些 I/O 操作。那么,考虑到所有这些,您如何编写程序?嗯,功能
readFile :: FilePath -> IO String
将文件读取为String
. 所以我们可以使用它来获取文件的内容。功能
lines:: String -> [String]
在换行符上拆分 a String
,所以现在你有一个String
s 列表,每个对应于文件的一行。功能
init :: [a] -> [a]
从列表中删除最后一个元素(这将摆脱.
每行的最后一个元素)。功能
read :: (Read a) => String -> a
接受 aString
并将其转换为任意 Haskell 数据类型,例如Int
or 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)
要回答npfedwards
有关应用于lines
的输出的评论readFile text.txt
,您需要意识到这readFile text.txt
会给您一个IO String
,并且只有当您将它绑定到一个变量(使用contents <-
)时,您才能访问底层String
,以便您可以应用lines
它。
记住:一旦你走了IO
,你就再也回不去了。
1我故意忽略unsafePerformIO
,因为正如名字所暗示的那样,它非常不安全!除非您真的知道自己在做什么,否则永远不要使用它。
作为一个编程菜鸟,我也被IO
s 弄糊涂了。请记住,如果你去IO
,你永远不会出来。克里斯写了一个很好的解释为什么。我只是认为给出一些关于如何IO String
在 monad 中使用的例子可能会有所帮助。我将使用getLine读取用户输入并返回一个IO String
.
line <- getLine
所有这一切都是将用户输入绑定getLine
到一个名为 的值line
。如果你在 ghci 中输入 this,然后输入:type line
它会返回:
:type line
line :: String
可是等等!getLine
返回一个IO String
:type getLine
getLine :: IO String
那么发生了什么IO
事呢getLine
?<-
是怎么回事。<-
是你的IO
朋友。它允许您带出被IO
monad 污染的值,并将其与您的正常功能一起使用。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
正如人们已经说过的那样,如果您有两个函数,一个是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
。
你为什么要那样做?好吧,有了这个,您将在您的语言中拥有纯粹且
引用透明的程序(函数)。这意味着每个类型为 IO-free 的函数都是纯的且引用透明的,因此对于相同的参数,它将返回相同的值。例如,doTheRightThingWithString
将为Something
相同的String
. 但是readStringFromFile
,不是 IO-free 的每次都可以返回不同的字符串(因为文件可以更改),因此您无法从IO
.
如果您有这种类型的解析器:
myParser :: String -> Foo
并且您使用
readFile "thisfile.txt"
然后您可以使用读取和解析文件
fmap myParser (readFile "thisfile.txt")
其结果将具有 type IO Foo
。
该fmap
装置myParser
在 IO 的“内部”运行。
另一种思考方式是,而myParser :: String -> Foo
, fmap myParser :: IO String -> IO Foo
。