90

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

4

5 回答 5

82

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

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

但是,如果资源受到限制(如在高性能场景中,或希望扩展到机器极限的生产环境中),依靠 GC 进行清理可能是一个不足的保证。

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

那么有哪些惰性 IO 替代方案并不意味着放弃增量处理(这反过来又会消耗太多资源)?好吧,我们有foldl基础处理,又名迭代器或枚举器,由Oleg Kiselyov 在 2000 年代后期引入,并被许多基于网络的项目推广。

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

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

于 2011-05-05T04:33:02.250 回答
41

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

迭代器作为编程模型的状态是一项正在进行的工作,但它甚至比一年前要好得多。我们正在学习哪些组合器有用(例如zip, breakE, enumWith),哪些不太有用,结果内置的迭代器和组合器不断提供更多的表现力。

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

于 2011-05-05T14:16:16.180 回答
25

我一直在生产代码中使用惰性 I/O。就像唐提到的那样,这只是在某些情况下才会出现的问题。但是对于仅仅阅读几个文件来说它工作正常。

于 2011-05-05T08:34:02.050 回答
21

更新:最近在 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 问题的三个示例

于 2012-10-29T20:37:52.523 回答
17

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

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

这些问题并非总是致命的,但这是另一个需要考虑的问题,而且我个人会避免懒惰的 IO,除非预先完成所有工作存在真正的问题。

于 2012-03-01T14:22:31.767 回答