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)也因“打开的文件太多”而失败