Haskell IO和关闭文件

时间:2008-11-17 20:30:30

标签: haskell file-io io

当我在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这样的函数?

6 个答案:

答案 0 :(得分:37)

正如其他人所说,这是因为懒惰的评价。此操作后手柄处于半关闭状态,并在读取所有数据时自动关闭。 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”。

答案 1 :(得分:4)

[更新: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> 

?!?!?

答案 2 :(得分:2)

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

有关详细信息,请参阅Real World Haskell中的the part about lazy I/O

答案 3 :(得分:1)

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

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

在Hugs中产生以下内容

> 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

答案 4 :(得分:1)

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

答案 5 :(得分:0)

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

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