懒惰I / O有什么不好?

我通常听说生产代码应该避免使用惰性I / O。 我的问题是,为什么? 使用Lazy I / O之外的东西可以玩吗? 什么使替代scheme(例如统计员)更好?

懒惰的IO有一个问题,即释放你获得的资源有点难以预料,因为它取决于你的程序如何使用数据 – 它的“需求模式”。 一旦程序放弃了对资源的最后一个引用,GC将最终运行并释放该资源。

懒惰的溪stream是一个非常方便的编程风格。这就是为什么shellpipe道是如此有趣和stream行。

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

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

那么,什么是懒惰的IO的select,不意味着放弃增量处理(反过来会消耗太多的资源)? 那么,我们有基于foldl的处理,又名迭代或枚举,由奥列格Kiselyov在21世纪后期引入,并由一些基于networking的项目推广。

我们不是将数据视为惰性数据stream,而是将数据作为一个大批量来处理,而是通过基于块的严格处理进行抽象,并在读取最后一个数据块时保证资源的最终化。 这是基于迭代编程的本质,也是一个非常好的资源约束。

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

Dons提供了一个非常好的答案,但他忽略了(对我来说)迭代最引人注目的特性之一:它们使得更容易理解空间pipe理,因为旧数据必须明确保留。 考虑:

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

这是一个众所周知的空间泄漏,因为整个列表xs必须保留在内存中以计算sumlength 。 通过创build一个折叠可以使一个高效的消费者:

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

但是对于每个stream处理器来说,这样做有点不方便。 有一些概括( Conal Elliott – 美丽的折叠压缩 ),但他们似乎并没有被抓住。 但是,迭代可以得到类似的expression级别。

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

这并不像折叠那样有效,因为列表仍然是迭代多次的,但是它是以块的方式收集的,所以旧数据可以被高效地垃圾收集。 为了打破这个属性,有必要明确地保留整个input,比如stream2list:

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

迭代作为一个编程模型的状态是一个正在进行的工作,但它比一年前要好得多。 我们正在学习什么组合器是有用的(例如zipbreakEenumWith ),哪些更less,结果是内置的迭代器和组合器不断提供更多的expression。

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

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

惰性IO的另一个问题到目前为止还没有被提及,它具有令人惊讶的行为。 在一个正常的Haskell程序中,有时很难预测程序的每个部分是什么时候被评估的,但幸运的是,由于纯度,除非你有性能问题,否则它并不重要。 当懒惰的IO被引入时,你的代码的评估顺序实际上会影响它的含义,所以你以前认为无害的改变会导致你真正的问题。

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

这些问题并不总是致命的,但是这是另外一回事,而且是一个足够严重的头痛,我个人应该避免懒惰的IO,除非事先做好所有的工作是有问题的。

更新:最近在Haskell咖啡馆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操作可能会延迟,直到太迟,例如在文件closures之后。 从Haskell维基引用- 惰性IO的问题 :

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

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

问题是在fileData被强制之前,Fileclosures句柄。 正确的方法是将所有代码传递给文件:

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

这里,数据在使用文件结束之前被消耗。

这通常是意想不到的,也是一个容易犯的错误。


另请参见: 惰性I / O问题的三个示例 。