34

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

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

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

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

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

4

4 回答 4

93

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

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

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

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

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

代码被标记为纯或不纯的方式是使用类型系统。当您看到类似的函数签名时

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动作中获取值呢?幸运的是,函数的类型mainIO ()(它是一个执行一些 I/O 并返回的操作(),这与什么都不返回相同)。所以你总是可以在IO里面使用你的函数main。当你执行一个 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]

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

init :: [a] -> [a]

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

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

接受 aString并将其转换为任意 Haskell 数据类型,例如Intor 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,因为正如名字所暗示的那样,它非常不安全!除非您真的知道自己在做什么,否则永远不要使用它。

于 2012-06-27T15:49:03.883 回答
13

作为一个编程菜鸟,我也被IOs 弄糊涂了。请记住,如果你去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朋友。它允许您带出被IOmonad 污染的值,并将其与您的正常功能一起使用。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
于 2012-06-28T02:39:06.883 回答
9

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

对于fmap(是)IOIOFunctor

fmap doTheRightThingWithString readStringFromFile

对于(<$>)(是和)IOIOApplicative(<$>) == fmap

import Control.Applicative

...

doTheRightThingWithString <$> readStringFromFile

对于liftM( IO) liftM == 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

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

于 2012-06-28T07:50:24.100 回答
5

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

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

于 2012-06-27T15:36:51.410 回答