懒惰和I/O如何在Haskell中协同工作?

Bil*_*ill 10 haskell lazy-evaluation

我正在努力深入了解Haskell中的懒惰.

我今天想象的是以下片段:

data Image = Image { name :: String, pixels :: String }

image :: String -> IO Image
image path = Image path <$> readFile path
Run Code Online (Sandbox Code Playgroud)

这里的吸引力是我可以简单地创建一个Image实例并传递它; 如果我需要图像数据,它将被懒惰地读取 - 如果不是,将避免读取文件的时间和内存成本:

 main = do
   image <- image "file"
   putStrLn $ length $ pixels image
Run Code Online (Sandbox Code Playgroud)

但它是如何实际工作的?懒惰如何与IO兼容?readFile是否会被调用,无论我是否访问,pixels image或者如果我从未引用它,运行时是否会将该thunk评估为未评估?

如果确实懒惰地读取了图像,那么I/O操作是否可能无序发生?例如,如果在调用后立即image删除文件怎么办?现在,putStrLn调用在尝试读取时将找不到任何内容.

C. *_*ann 17

懒惰如何与I/O兼容?

简短回答:事实并非如此.


答案IO很长:行动是严格按顺序排列的,这几乎是你想到的原因.当然,对结果进行的任何纯计算都可能是懒惰的; 例如,如果您读入文件,进行一些处理,然后打印出一些结果,则可能不会评估输出不需要的任何处理.但是,整个文件将被读取,甚至是您从不使用的部分.如果你想要懒惰的I/O,你有两个选择:

  • 滚动你自己的显式延迟加载例程,就像你在任何严格的语言中一样.似乎令人讨厌,被授予,但另一方面,Haskell制作了一个严格的,严格的命令式语言.如果你想尝试新的和有趣的东西,试试看Iteratees.

  • 作弊骗子作弊.hGetContents你这样的函数会为你做懒惰的按需I/O,没有问题.有什么收获?它(技术上)打破了参考透明度.纯代码可以间接导致副作用,如果您的代码真的错综复杂,可能会发生涉及副作用排序的有趣事情.hGetContents和朋友们一起使用unsafeInterleaveIO,这就是......正如它所说的那样.它远没有像使用那样在你的脸上爆炸unsafePerformIO,但请考虑自己警告.

  • @Bill:这是readFile的实现,直接来自GHC的标准库:`readFile name = openFile name ReadMode >> = hGetContents`所以不,你的例子属于"作弊骗子"类别.也就是说,懒惰的I/O功能通常足够安全,适合大多数日常实际使用,因此除非纯度对您非常重要,否则不要过多地使用它. (5认同)
  • @Reid Barton:`unsafeInterleaveIO`的问题在于它让非确定性可能以有限的方式泄漏出来.据我所知,没有人证明这个特定的问题甚至可以在一些合理的代码中引起有意义的问题,但这仅仅是"缺乏反例证明".奥列格的一个纯粹函数的设想例子,其结果取决于其参数的评估顺序,对我来说原则上足够有说服力.实际上,它并不那么重要. (4认同)

Nor*_*sey 9

懒惰的I/O打破了Haskell的纯度.结果readFile确实是根据需要懒洋洋地产生的.I/O操作发生的顺序不固定,所以是的,它们可能"乱序".在拉动像素之前删除文件的问题是真实的.简而言之,懒惰的I/O非常方便,但它是一个边缘非常锋利的工具.

关于真实世界Haskell的书对懒惰I/O进行了长时间的处理,并解决了一些陷阱.