懒惰I / O有什么坏处?

时间:2011-05-05 04:22:51

标签: haskell io lazy-evaluation

我一般都听说生产代码应避免使用Lazy I / O.我的问题是,为什么?是否可以在懒散的I / O之外使用它?什么使替代品(例如调查员)变得更好?

5 个答案:

答案 0 :(得分:79)

懒惰的IO有一个问题,即释放你获得的任何资源有些不可预测,因为它取决于你的程序如何消耗数据 - 它的“需求模式”。一旦您的程序删除了对资源的最后一个引用,GC将最终运行并释放该资源。

懒惰流是一种非常方便的程序样式。这就是shell管道如此有趣和受欢迎的原因。

但是,如果资源受到限制(如在高性能方案中,或者预期会扩展到机器极限的生产环境),依靠GC进行清理可能是一个不充分的保证。

有时您必须急切地释放资源,以提高可扩展性。

那么什么是懒惰IO的替代方案并不意味着放弃增量处理(反过来会消耗太多资源)?好吧,我们有基于foldl的处理,也就是迭代或枚举,由 Oleg Kiselyov in the late 2000s 引入,并且已经被许多基于网络的项目所推广。

我们不是将数据作为延迟流处理,而是在一个庞大的批处理中,而是通过基于块的严格处理进行抽象,并在读取最后一个块后保证资源的最终化。这是基于iteratee的编程的本质,也是提供非常好的资源约束的。

基于iteratee的IO的缺点是它有一个有点笨拙的编程模型(大致类似于基于事件的编程,而不是基于线程的良好控制)。在任何编程语言中,它绝对是一种先进的技术。而对于绝大多数编程问题,懒惰IO完全令人满意。但是,如果要打开许多文件,或者在许多套接字上进行交谈,或者使用许多同时使用的资源,那么迭代(或枚举)方法可能会有意义。

答案 1 :(得分:37)

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),哪些不那么重要,结果是内置的迭代器和组合器提供了更多的表达能力。< / p>

那就是说,Dons是正确的,他们是一种先进的技术;我当然不会将它们用于每个I / O问题。

答案 2 :(得分:24)

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

答案 3 :(得分:18)

更新:最近在haskell-cafe Oleg Kiseljov showedunsafeInterleaveST(用于在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 - Problems with lazy 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完成之前消耗。

这通常是意料之外的,也是一个容易犯的错误。


另请参阅:Three examples of problems with Lazy I/O

答案 4 :(得分:17)

迄今为止尚未提及的懒惰IO的另一个问题是它具有令人惊讶的行为。在正常的Haskell程序中,有时很难预测程序的每个部分何时被评估,但幸运的是,由于纯度,除非遇到性能问题,否则它确实无关紧要。当引入惰性IO时,代码的评估顺序实际上会对其含义产生影响,因此您习惯认为无害的更改可能会导致真正的问题。

作为一个例子,这里有一个关于代码看起来合理但被延迟IO更加混乱的问题:withFile vs. openFile

这些问题并非总是致命的,但这是另一件需要考虑的事情,而且是一个足够严重的头痛,我个人避免懒惰的IO,除非事先做好所有工作是一个真正的问题