我正在努力深入了解Haskell中的懒惰。
我今天想象的是以下片段:
data Image = Image { name :: String, pixels :: String }
image :: String -> IO Image
image path = Image path <$> readFile path
这里的吸引力是我可以简单地创建一个Image实例并传递它;如果我需要图像数据,它将被懒惰地读取 - 如果不是,将避免读取文件的时间和内存成本:
main = do
image <- image "file"
putStrLn $ length $ pixels image
但它是如何实际运作的?懒惰如何与IO兼容?无论我是否访问pixels image
,还是运行时都会调用readFile,如果我从未引用它,那么运行时是否会重新评估thunk?
如果确实懒惰地读取了图像,那么I / O操作是否可能无序发生?例如,如果在调用image
后立即删除文件怎么办?现在,putStrLn调用在尝试读取时将找不到任何内容。
答案 0 :(得分:17)
懒惰如何与I / O兼容?
简短回答:不是。
长答案:IO
行动是严格按顺序排列的,原因很多就是你想到的。当然,对结果进行的任何纯计算都可能是懒惰的;例如,如果您读入文件,进行一些处理,然后打印出一些结果,则可能不会评估输出不需要的任何处理。但是,整个文件将被读取,甚至是您从不使用的部分。如果你想要懒惰的I / O,你有两个选择:
滚动您自己的显式延迟加载例程,就像您在任何严格的语言中一样。似乎令人讨厌,被授予,但另一方面,Haskell制作了一个严格的,严格的命令式语言。如果您想尝试新的有趣的事情,请尝试查看Iteratees。
作弊骗子作弊。函数such as hGetContents
将为您执行惰性,按需I / O,无需提问。有什么收获?它(技术上)打破了参考透明度。纯代码可以间接导致副作用,如果您的代码真的错综复杂,可能会发生涉及副作用排序的有趣事情。 hGetContents
和朋友已经实施了using unsafeInterleaveIO
,这正是它在锡上所说的。与使用unsafePerformIO
相比,它在你的脸上不太可能爆炸,但请注意自己警告。
答案 1 :(得分:9)
懒惰的I / O打破了Haskell的纯度。 readFile
的结果确实是根据需要懒散地产生的。 I / O操作发生的顺序不固定,所以是的,它们可能“乱序”。在拉动像素之前删除文件的问题是真实的。简而言之,懒惰的I / O非常方便,但它是一种边缘非常锋利的工具。
关于真实世界哈斯克尔的书有一个lengthy treatment of lazy I/O,并且讨论了一些陷阱。