Haskell return 来自文件 IO 的惰性字符串

Haskell return lazy string from file IO

我又回来了,带着我最新杰作的(对我来说)非常奇怪的行为...

这段代码应该读取一个文件,但它没有:

readCsvContents :: String -> IO ( String )
readCsvContents fileName = do
     withFile fileName ReadMode (\handle -> do
          contents <- hGetContents handle
          return contents
          )

main = do
    contents <- readCsvContents "src\EURUSD60.csv"
    putStrLn ("Read " ++ show (length contents) ++ " Bytes input data.")

结果是

Read 0 Bytes input data.

现在我改了第一个函数,加了一个putStrLn:

readCsvContents :: String -> IO ( String )
readCsvContents fileName = do
     withFile fileName ReadMode (\handle -> do
          contents <- hGetContents handle
          putStrLn ("hGetContents gave " ++ show (length contents) ++ " Bytes of input data.")
          return contents
          )

结果是

hGetContents gave 3479360 Bytes of input data.
Read 3479360 Bytes input data.

卧槽???嗯,我知道,Haskell 是懒惰的。但我不知道我不得不这样踢它的屁股。

你说得对,这很痛苦。出于这个原因,避免使用旧的标准文件 IO 模块——除了像您所做的那样简单地读取一个不会改变的整个文件;这可以用 readFile.

来完成
readCsvContents :: Filepath -> IO String
readCsvContents fileName = do
   contents <- readFile fileName
   return contents

请注意,根据 monad 法则,这与1 完全相同

readCsvContents = readFile

您尝试的问题是当 monad 退出时句柄被无条件关闭 withFile,而没有检查 contents 的惰性求值是否实际上强制了文件读取。那当然是可怕的;我永远不会费心自己使用手柄。 readFile 通过将句柄的关闭与原始结果 thunk 的垃圾收集联系起来避免了这个问题;这也不是很好,但通常效果很好。

为了正确使用文件 IO,请查看 conduit or pipes 库。前者更注重性能,后者更注重优雅(但实际上,区别并不大)。


1而您的第一次尝试与 readCsvContents fn = withFile fn ReadMode hGetContents 相同。

这是惰性IO的问题。您的代码中发生的是 withFile 打开文件,将句柄传递给 lambda。这个 lambda returns 一个包含文件内容的惰性列表。然后 withFile 注意到回调完成并关闭文件。

由于返回的列表是惰性的,只有在评估列表时才会读取文件内容。这发生在对 length 的调用中。但是,此时文件句柄已经关闭,因此您无法从文件中读取任何内容。

您调用的修改版本在 withFile 参数中强制文件内容,此时文件仍然可用,因此它可以工作。