Haskell IO 和关闭文件

Posted

技术标签:

【中文标题】Haskell IO 和关闭文件【英文标题】:Haskell IO and closing files 【发布时间】:2010-09-22 17:54:07 【问题描述】:

当我在 Haskell 中打开一个文件进行读取时,我发现关闭它后我无法使用该文件的内容。例如,这个程序将打印一个文件的内容:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          putStr contents
          hClose inFile

我预计将putStr 行与hClose 行互换不会产生任何效果,但是这个程序什么也不打印:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents inFile
          hClose inFile
          putStr contents

为什么会这样?我猜这与惰性评估有关,但我认为这些表达式会被排序,所以不会有问题。你将如何实现像readFile 这样的函数?

【问题讨论】:

【参考方案1】:

这里的解释相当长。请原谅我只提供一个简短的提示:您需要阅读“半封闭文件句柄”和“unsafePerformIO”。

简而言之 - 这种行为是语义清晰和惰性评估之间的设计折衷。你应该推迟 hClose 直到你完全确定你不会对文件内容做任何事情(比如,在错误处理程序中调用它,或者类似的东西),或者使用除了 hGetContents 之外的其他东西来非懒惰地获取文件内容。

【讨论】:

你能链接任何关于这些主题的好东西吗?除了关于特定问题的稀疏文档和邮件列表消息外,我找不到太多其他信息。 我认为unsafePerformIO 与此无关。也许unsafeInterleaveIO.【参考方案2】:

这是因为 hGetContents 还没有做任何事情:它是惰性 I/O。只有当您使用结果字符串时,才会实际读取文件(或需要的部分)。如果要强制读取它,可以计算它的长度,并使用 seq 函数强制计算长度。惰性 I/O 可能很酷,但也可能令人困惑。

如需了解更多信息,请参阅 Real World Haskell 中的the part about lazy I/O。

【讨论】:

【参考方案3】:

如前所述,hGetContents 是懒惰的。 readFile 是严格的,完成后关闭文件:

main = do contents <- readFile "foo"
          putStr contents

在拥抱中产生以下效果

> main
blahblahblah

foo 在哪里

blahblahblah

有趣的是,seq 只会保证读取输入的部分,而不是全部:

main = do inFile <- openFile "foo" ReadMode
          contents <- hGetContents $! inFile
          contents `seq` hClose inFile
          putStr contents

产量

> main
b

一个好的资源是:Making Haskell programs faster and smaller: hGetContents, hClose, readFile

【讨论】:

readFile 使用 hGetContents 并且不关闭文件。根据 Real World Haskell 和源代码本身,它很懒惰。 首先,readFile 并不严格,如前所述,其次,$!hGetContents 的使用完全是多余的。【参考方案4】:

正如其他人所说,这是因为懒惰的评估。此操作后句柄处于半关闭状态,读取完所有数据后将自动关闭。 hGetContents 和 readFile 都以这种方式偷懒。如果您在句柄保持打开时遇到问题,通常您只需强制读取。这是简单的方法:

import Control.Parallel.Strategies (rnf)
-- rnf means "reduce to normal form"
main = do inFile <- openFile "foo" 
          contents <- hGetContents inFile
          rnf contents `seq` hClose inFile -- force the whole file to be read, then close
          putStr contents

然而,这些天来,没有人再将字符串用于文件 I/O。新方法是使用 Data.ByteString(在 hackage 上可用)和 Data.ByteString.Lazy,当您想要延迟读取时。

import qualified Data.ByteString as Str

main = do contents <- Str.readFile "foo"
          -- readFile is strict, so the the entire string is read here
          Str.putStr contents

ByteStrings 是处理大字符串(如文件内容)的方法。它们比 String (= [Char]) 更快,内存效率更高。

注意事项:

我从 Control.Parallel.Strategies 导入 rnf 只是为了方便。您可以很容易地自己编写类似的内容:

  forceList [] = ()
  forceList (x:xs) = forceList xs

这只是强制遍历列表的脊椎(而不是值),这将具有读取整个文件的效果。

懒惰的 I/O 被专家们认为是邪恶的;我建议暂时对大多数文件 I/O 使用严格的字节串。烤箱中有一些解决方案试图恢复可组合的增量读取,其中最有希望的是 Oleg 称为“Iteratee”。

【讨论】:

两个厘米。首先,很多人仍然使用字符串进行文件 IO。当您想从文件中获取的是字符串时,它们非常好!其次,Lazy IO 并不被很多人认为是邪恶的,但它被认为是棘手的。它让我们能够以非常低的句法开销完成各种简洁的事情,但代价是在维护等式推理的同时维护某些有限类型的操作推理。 遇到了这个答案,谢谢@liqui!只是想指出(3 年后)你的rnf 应该是:rnf contents 'seq' hClose inFile,在seq 周围加上反引号。此外,rnf 已移至 Control.DeepSeq @Peter,我想我们在谈论 lazy IO,你的评论没有提到。 “严重的服务器端编程中的惰性 IO 是不专业的” – Oleg Kiselyov【参考方案5】:

[更新:Prelude.readFile 会导致如下所述的问题,但切换到使用 Data.ByteString 的版本一切正常:我不再遇到异常。]

这里是 Haskell 新手,但目前我不相信“readFile 是严格的,并在完成后关闭文件”的说法:

go fname = do
   putStrLn "reading"
   body <- readFile fname
   let body' = "foo" ++ body ++ "bar"
   putStrLn body' -- comment this out to get a runtime exception.
   putStrLn "writing"
   writeFile fname body'
   return ()

这适用于我正在测试的文件,但是如果您注释掉 putStrLn,那么显然 writeFile 会失败。 (有趣的是,Haskell 异常消息是多么蹩脚,缺少行号等?)

Test> go "Foo.hs"
reading
writing
Exception: Foo.hs: openFile: permission denied (Permission denied)
Test> 

?!?!?

【讨论】:

我刚刚运行了你的代码。 GHCI 说:openFile: resource busy (file is locked)。这与 readFile 的惰性是一致的。【参考方案6】:

如果您想让您的 IO 保持惰性,但要安全地执行此操作,以免发生此类错误,请使用为此设计的包,例如 safe-lazy-io。 (但是,safe-lazy-io 不支持字节串 I/O。)

【讨论】:

以上是关于Haskell IO 和关闭文件的主要内容,如果未能解决你的问题,请参考以下文章

为啥我使用 iteratee IO 的 Mapreduce 实现(真实世界的 Haskell)也因“打开的文件太多”而失败

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

带有 Haskell repa 数组库的彩色图像文件 IO

阅读和学习的好 Haskell 源 [关闭]

我可以确保 Haskell 执行原子 IO 吗?

使用 Haskell 播放 wav 文件