我在Haskell中实现了withFile
:
withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile' path iomode f = do
handle <- openFile path iomode
result <- f handle
hClose handle
return result
当我运行Learn You a Haskell提供的main
时,它会打印出&#34; girlfriend.txt,&#34;的内容。正如所料:
import System.IO
main = do
withFile' "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
我不确定我的withFile'
是否会使用最后两行:(1)关闭句柄,(2)将结果作为IO a
返回。
为什么没有发生以下情况?
result
懒惰地绑定到f handle
hClose handle
关闭文件句柄result
得到return
&#39; d,这会导致f handle
的实际评估。由于handle
已关闭,因此会抛出错误。答案 0 :(得分:5)
懒惰IO通常被称为令人困惑。
注意第一次和第二次使用之间的区别(括号是不必要的,但在第二个例子中澄清)。
ghci> withFile' "temp.hs" ReadMode (hGetContents >=> putStr) -- putStr
import System.IO
import Control.Monad
withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile' path iomode f = do
handle <- openFile path iomode
result <- f handle
hClose handle
return result
ghci> (withFile' "temp.hs" ReadMode hGetContents) >>= putStr
ghci>
在这两种情况下,传入的f
都有机会在句柄关闭之前运行。由于延迟评估,hGetContents
仅在需要时才读取文件,即强制为了产生某些其他函数的输出。
在第一个示例中,由于f
为(hGetContents >=> putStr)
,因此必须读取文件的完整内容才能执行putStr
。
在第二个示例中,不需要在hGetContents
之后对return result
进行评估,这是一个惰性列表。 (我很高兴return (show [1..])
如果我选择使用整个输出,它将无法终止。)这被视为懒惰IO的一个问题,它由诸如严格IO,管道或管道之类的替代方案修复。
当句柄过早关闭时,可能返回文件的空字符串是一个错误,但在关闭它之前肯定运行整个f
不是。
答案 1 :(得分:3)
等式推理意味着您可以通过内联和替换事物来推理Haskell代码(有一些警告,但它们不适用于此处)。
这意味着我需要做的就是了解您的代码,就是在withFile'
点击:
import System.IO
main = do
withFile' "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
...并内联其定义:
main = do
handle <- openFile "girlfriend.txt" ReadMode
contents <- hGetContents handle
result <- putStr contents
hClose handle
return result
一旦你内联它的定义,就会更容易看到发生了什么。 putStr
在关闭句柄之前评估文件的整个contents
,因此没有错误。此外,result
不是您认为的那样:它是putStr
的返回值,它只是()
,而不是文件的内容。
答案 2 :(得分:2)
大多数IO动作都不会延迟执行。
IO操作执行与正常的Haskell 评估值不同。 IO执行只能由尝试执行main
的所有效果的外部驱动程序执行;它按照IO动作的monadic排序所暗示的正确顺序进行。
驱动程序需要知道下一个IO操作最终会触发Haskell中所有惰性值的评估;如果它对未评估的惰性值感到满意并且在没有完全评估和执行它的情况下转移到下一个事情,那么它将使main
没有评估,并且没有Haskell程序可以做任何事情。
执行IO操作所产生的Haskell值当然可能是未评估的惰性值,但每个IO操作本身都由驱动程序进行评估和执行(包括使用do block或binding进行排序的所有子操作)。
所以result
懒得与f handle
完全没有评价;评估f handle
以提出子操作hGetContents handle
和putStr contents
。在外部驱动程序转移到hClose handle
之前,它们都已完全执行,所以一切都没问题。
但请注意,hGetContents很特别。引自文档:
计算hGetContents hdl返回与hdl管理的通道或文件的未读部分对应的字符列表,该列表被置于中间状态,半封闭。在这种状态下,hdl实际上是关闭的,但是项目是根据需要从hdl读取并累积在hGetContents hdl返回的特殊列表中。
由于句柄处于关闭状态而失败的任何操作,如果句柄处于半关闭状态,也会失败。唯一的例外是hClose。半封闭的手柄关闭:
- 如果应用了hClose;
- 如果从句柄中读取项目时发生I / O错误;
- 或读取句柄的全部内容后。
一旦半闭合的手柄关闭,相关列表的内容就会固定。此最终列表的内容仅部分指定:它将至少包含在句柄关闭之前评估的流的所有项目。
因此执行hGetContents handle
实际上会导致部分评估的列表,其懒惰评估与引擎盖下的其他IO操作相关联。如果不使用不安全的操作系列,这是不可能做到的,因为它基本上绕过了类型系统,并且可能导致你所关心的那种问题;如果您尝试过以下代码:
main = do
text <- withFile' "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
return contents)
putStr text
(传递给withFile'
的函数尝试返回文件内容,并在putStr
调用后传递给withFile'
,然后putStr
将是在 hClose
之后执行了,并且该文件在关闭之前可能尚未完全读取。