Haskell 什么';懒惰I/O有什么不好?

Haskell 什么';懒惰I/O有什么不好?,haskell,io,lazy-evaluation,Haskell,Io,Lazy Evaluation,我通常听说生产代码应该避免使用惰性I/O。我的问题是,为什么?除了玩游戏之外,还可以使用懒惰的I/O吗?是什么让备选方案(例如枚举数)更好呢?懒惰IO的问题是,释放您获得的任何资源都有点不可预测,因为这取决于您的程序如何使用数据——它的“需求模式”。一旦您的程序删除了对资源的最后一个引用,GC最终将运行并释放该资源 惰性流是一种非常方便的编程方式。这就是为什么贝壳管如此有趣和流行的原因 但是,如果资源受限(如在高性能场景中,或在预期扩展到机器极限的生产环境中),依赖GC进行清理可能是不够的保证

我通常听说生产代码应该避免使用惰性I/O。我的问题是,为什么?除了玩游戏之外,还可以使用懒惰的I/O吗?是什么让备选方案(例如枚举数)更好呢?

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

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

但是,如果资源受限(如在高性能场景中,或在预期扩展到机器极限的生产环境中),依赖GC进行清理可能是不够的保证

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

那么,除了懒惰IO之外,还有哪些替代方案不意味着放弃增量处理(这反过来会消耗太多资源)?好的,我们有基于
foldl
的处理,也称为迭代器或枚举器,由引入,此后被许多基于网络的项目推广

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


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

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

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

average :: [Float] -> Float
average xs = sum xs / length xs
这是一个众所周知的空间泄漏,因为整个列表
xs
必须保留在内存中才能计算
sum
length
。通过创建一个折页,可以创建一个高效的消费者:

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'
average2::[Float]->Float
平均2 xs=未修剪(/)折叠(\(sumT,n)x->(sumT+x,n+1))(0,0)xs
--注意:这将建立书面的thunks,使用严格的配对和foldl'
但是对于每个流处理器来说,这样做有点不方便。有一些概括(),但它们似乎没有流行起来。但是,迭代对象可以获得类似级别的表达式

aveIter = uncurry (/) <$> I.zip I.sum I.length
aveIter=uncurry(/)I.zip I.sum I.length
这不如折叠效率高,因为列表仍要重复多次,但它是分块收集的,因此旧数据可以高效地进行垃圾收集。为了破坏该属性,必须显式保留整个输入,例如使用stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
badAveIter=(\xs->sum xs/length xs)I.stream2list
iteratees作为编程模型的状态是一项正在进行的工作,但是它甚至比一年前要好得多。我们正在学习哪些组合词有用(例如,
zip
breakE
enumWith
),而哪些组合词不那么有用,其结果是内置迭代器和组合词不断提供更高的表达能力


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

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

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


这些问题并非总是致命的,但这是另一个需要考虑的问题,而且是一个非常严重的头痛问题,我个人会避免懒惰的IO,除非所有的前期工作都有真正的问题。

更新:最近在haskell cafe上发布的
unsafeInterleaveST
(用于在ST monad中实现惰性IO)是非常不安全的-它破坏了等式推理。他表明它允许构造
bad\u ctx::((Bool,Bool)->Bool)->Bool
以致

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
即使
==
是可交换的



惰性IO的另一个问题是:实际的IO操作可能会推迟到太晚,例如在文件关闭后。引用自:

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

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData
错误=do
文件数据做什么

fileData我也使用惰性I/O。当我想要更多地控制资源管理时,我会求助于迭代者。实际上,将
hGetContents
与file
组合是毫无意义的,因为前者将句柄置于“伪关闭”状态,并将为您(惰性地)处理关闭因此,代码完全等同于
readFile
,甚至是
openFile
,而没有
hClose
。这基本上就是惰性I/O。如果
right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData