【问题标题】:In Haskell, I want to read a file and then write to it. Do I need strictness annotation?在 Haskell 中,我想读取一个文件然后写入它。我需要严格注释吗?
【发布时间】:2011-02-01 09:44:50
【问题描述】:

对 Haskell 来说还是很陌生..

我想读取一个文件的内容,对它做一些可能涉及 IO 的事情(现在使用 putStrLn),然后将新内容写入同一个文件。

我想出了:

doit :: String -> IO ()
doit file = do
    contents <- withFile tagfile ReadMode $ \h -> hGetContents h
    putStrLn contents
    withFile tagfile WriteMode $ \h -> hPutStrLn h "new content"

但是由于懒惰,这不起作用。不打印文件内容。我找到了this post,它很好地解释了它。

建议的解决方案是在withFile 中包含putStrLn

doit :: String -> IO ()
doit file = do
    withFile tagfile ReadMode $ \h -> do
        contents <- hGetContents h
        putStrLn contents
    withFile tagfile WriteMode $ \h -> hPutStrLn h "new content"

这行得通,但这不是我想做的。 I will finally replace putStrLn 中的操作可能会很长,我不想一直保持文件打开。一般来说,我只是希望能够取出文件内容,然后在处理该内容之前将其关闭。

我想出的解决方案如下:

doit :: String -> IO ()
doit file = do
    c <- newIORef ""
    withFile tagfile ReadMode $ \h -> do
        a <- hGetContents h
        writeIORef c $! a
    d <- readIORef c
    putStrLn d
    withFile tagfile WriteMode $ \h -> hPutStrLn h "Test"

但是,我发现这很长而且有点混淆。我认为我不需要IORef 只是为了获得一个价值,但我需要“地方”来放置文件内容。此外,如果没有 writeIORef 的严格性注释 $!,它仍然无法工作。我猜IORefs 天生不严谨?

谁能推荐一种更好、更短的方法来做到这一点,同时保持我想要的语义?

谢谢!

【问题讨论】:

  • 如果您发布编译代码所需的import 声明,其他人可能会帮助调试它...
  • 过早的优化,邪恶的根源等等。为什么你害怕保留文件描述符,只要你需要它?
  • IORef 没有您感知到的效果。 IORef 中的值可以像从块中返回的值一样懒惰。您的代码相当于也不起作用: doit file = do { d do { a

标签: haskell file-io io lazy-evaluation


【解决方案1】:

您的第一个程序不起作用的原因是withFile 在执行传递给它的 IO 操作后关闭了文件。在您的情况下,IO 操作是 hGetContents,它 立即读取文件,但仅在需要其内容时才读取。当您尝试打印文件内容时,withFile 已经关闭了文件,因此读取失败(静默)。

您可以通过不重新发明轮子并简单地使用readFilewriteFile 来解决这个问题:

doit file = do
    contents <- readFile file
    putStrLn contents
    writeFile file "new content"

但假设您希望新内容依赖于旧内容。那么你通常不能简单地做

doit file = do
    contents <- readFile file
    writeFile file $ process contents

因为writeFile 可能会影响readFile 返回的内容(请记住,它还没有真正读取文件)。或者,根据您的操作系统,您可能无法在两个单独的句柄上打开同一个文件进行读取和写入。简单但丑陋的解决方法是

doit file = do
    contents <- readFile file
    length contents `seq` (writeFile file $ process contents)

这将强制readFile 读取整个文件并在writeFile 操作开始之前将其关闭。

【讨论】:

  • 阅读您链接到的帖子后,我想您已经知道其中的一些内容。但由于这里有不止一个懒惰问题,我认为最好彻底。
  • 而不是使用'长度内容seq ...'。我认为你可以使用 BangPatterns 扩展并将上一行重写为 '!contents
  • 相当于'contents seq ...',这还不够:它只会评估contents的顶级构造函数(即是否为空),也就是只计算第一个将读取文件的块。
【解决方案2】:

我认为解决这个问题最简单的方法是使用严格的 IO:

import qualified System.IO.Strict as S
main = do
    file <- S.readFile "filename"
    writeFile "filename" file

【讨论】:

    【解决方案3】:

    您可以复制文件句柄,对原始文件进行惰性写入(到文件末尾),对另一个文件进行惰性读取。因此,在附加到文件的情况下不涉及严格性注释。

    import System.IO
    import GHC.IO.Handle
    
    main :: IO ()
    main = do
        h <- openFile "filename" ReadWriteMode
        h2 <- hDuplicate h
    
        hSeek h2 AbsoluteSeek 0
        originalFileContents <- hGetContents h2
        putStrLn originalFileContents
    
        hSeek h SeekFromEnd 0
        hPutStrLn h $ concatMap ("{new_contents}" ++) (lines originalFileContents)
    
        hClose h2
        hClose h
    

    hDuplicate 函数由 GHC.IO.Handle 模块提供。

    返回原始句柄的副本,带有自己的缓冲区。然而,这两个句柄将共享一个文件指针。在复制句柄之前,会刷新原始句柄的缓冲区,包括丢弃任何输入数据。

    使用hSeek,您可以在读取或写入之前设置句柄的位置。

    但我不确定使用“AbsoluteSeek 0”而不是“SeekFromEnd 0”进行写入(即覆盖内容)的可靠性如何。通常我会建议先写入一个临时文件,例如使用 openTempFile(来自 System.IO),然后替换原始文件。

    【讨论】:

      【解决方案4】:

      这很难看,但是您可以通过询问输入的length 并在 do-block 中的下一条语句中使用seq'ing 来强制读取内容。但真正的解决方案是使用严格版本的hGetContents。我不知道它叫什么。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-05
        相关资源
        最近更新 更多