惰性I / O有什么不好?


89

我通常听说生产代码应避免使用惰性I / O。我的问题是,为什么?在玩弄之外使用Lazy I / O可以吗?是什么使替代方法(例如枚举数)更好?

Answers:


81

惰性IO的问题在于,释放所获取的任何资源在某种程度上都是不可预测的,因为这取决于程序如何使用数据-其“需求模式”。一旦您的程序删除了对该资源的最后一个引用,GC最终将运行并释放该资源。

惰性流是一种非常方便的编程风格。这就是为什么shell管道如此有趣和流行的原因。

但是,如果资源受到限制(例如在高性能方案中,或者期望扩展到机器极限的生产环境中),则依靠GC清理可能是不足的保证。

有时您必须急于释放资源,以提高可伸缩性。

那么,什么是懒惰IO的替代方案,而不是放弃增量处理(反过来又会消耗太多资源)呢?好吧,我们有foldl基础的处理程序(又名迭代器或枚举器),由Oleg Kiselyov在2000年代后期引入,并在随后的许多基于网络的项目中得到推广。

而不是将数据作为惰性流或成批处理,而是对基于块的严格处理进行了抽象,一旦读取了最后一个块,就保证了资源的最终确定。这是基于Iteratee的编程的本质,并且提供了非常好的资源约束。

基于iteratee的IO的缺点是它的编程模型有些笨拙(大致类似于基于事件的编程,而不是基于线程的漂亮控制)。在任何编程语言中,它绝对是一种先进的技术。对于绝大多数编程问题,惰性IO完全令人满意。但是,如果您要打开许多文件,或在许多套接字上交谈,或以其他方式同时使用许多资源,则iteratee(或枚举器)方法可能有意义。


22
由于我只是从懒惰I / O的讨论中获得了这个老问题的链接,因此我想加一点说明,即从那时起,迭代器的许多笨拙就被管道管道之类的新流媒体库所取代。
与Orjan约翰森

40

Dons提供了一个很好的答案,但是他遗漏了(对我而言)迭代对象最引人注目的功能之一:由于必须明确保留旧数据,因此它们使推理空间管理变得更加容易。考虑:

average :: [Float] -> Float
average xs = sum xs / length xs

这是众所周知的空间泄漏,因为xs必须将整个列表保留在内存中才能计算sumlength。通过创建折页可以使有效的消费者受益:

average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'

但是必须为每个流处理器执行此操作有点不方便。有一些概括(Conal Elliott-Beautiful Fold Zipping),但似乎没有流行。但是,迭代器可以使您获得类似的表达水平。

aveIter = uncurry (/) <$> I.zip I.sum I.length

这并不像折叠一样有效,因为该列表仍然需要重复多次,但是它是按块收集的,因此可以有效地对旧数据进行垃圾收集。为了破坏该属性,必须显式保留整个输入,例如使用stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

作为编程模型的迭代状态尚在开发中,但是比一年前要好得多。我们所学的组合子是有用的(例如zipbreakEenumWith),并且要少一些,其结果是,内置iteratees和组合程序提供持续更表现力。

就是说,唐斯(Don)是正确的,因为他们是一种先进的技术。我当然不会将它们用于每个I / O问题。


25

我一直在生产代码中使用惰性I / O。就像唐提到的那样,在某些情况下这只是一个问题。但是只读取一些文件就可以了。


我也使用惰性I / O。当我想对资源管理进行更多控制时,请转向迭代。
约翰L

20

更新:最近在haskell-cafe上,Oleg Kiseljov表示unsafeInterleaveST(用于在ST monad中实现懒惰IO)非常不安全-破坏了公式推理。他表明,它允许构造bad_ctx :: ((Bool,Bool) -> Bool) -> Bool 这样

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

即使==是可交换的。


惰性IO的另一个问题:实际的IO操作可以推迟到为时已晚,例如在关闭文件之后。从Haskell Wiki报价-惰性IO问题

例如,一个常见的初学者错误是在完成读取文件之前关闭文件:

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

问题是withFile在强制fileData之前关闭了句柄。正确的方法是将所有代码传递给withFile:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

在这里,数据在withFile完成之前被消耗掉。

这通常是出乎意料的,并且容易出错。


另请参阅:懒惰I / O问题的三个示例


其实结合hGetContentswithFile是毫无意义的,因为前者会将手柄“伪关闭”状态,并且将处理关闭你(懒洋洋),这样的代码是完全等同于readFile,甚至openFile没有hClose。这基本上是懒惰的I / O。如果您不使用readFilegetContents或者hGetContents您没有使用惰性I / O。例如line <- withFile "test.txt" ReadMode hGetLine工作正常。
2013年

1
@达格:虽然 hGetContents可以为您处理关闭文件,但是也可以“尽早”关闭它,并有助于确保可预测地释放资源。
本·米尔伍德

17

到目前为止尚未提及的惰性IO的另一个问题是它的行为令人惊讶。在普通的Haskell程序中,有时可能难以预测何时评估程序的每个部分,但是幸运的是,由于纯度的考虑,除非您遇到性能问题,否则实际上并不重要。引入惰性IO时,代码的评估顺序实际上会影响其含义,因此习惯于认为无害的更改可能会导致真正的问题。

例如,这是一个关于代码的问题,它看起来合理,但由于延迟的IO而变得更加混乱:withFile与openFile

这些问题并非总是致命的,但这是又一回事,而且头痛得非常严重,我个人避免懒惰的IO,除非预先进行所有工作确实存在问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.