Dan*_*ton 86 io haskell lazy-evaluation
我一般都听说生产代码应该避免使用Lazy I/O. 我的问题是,为什么?是否可以在懒散的I/O之外使用它?什么使替代品(例如调查员)更好?
Don*_*art 80
懒惰的IO有一个问题,即释放你获得的任何资源有些不可预测,因为它取决于你的程序如何消耗数据 - 它的"需求模式".一旦您的程序删除了对资源的最后一个引用,GC将最终运行并释放该资源.
懒惰的流是一种非常方便的编程风格.这就是外壳管道如此有趣和受欢迎的原因.
但是,如果资源受到限制(如在高性能方案中,或者期望扩展到机器极限的生产环境),依靠GC进行清理可能是一个不充分的保证.
有时您必须急切地释放资源,以提高可伸缩性.
那么什么是懒惰IO的替代方案并不意味着放弃增量处理(反过来会消耗太多资源)?好吧,我们有foldl基于处理,也就是迭代或调查员,由Oleg Kiselyov在21世纪后期引入,并且自从许多基于网络的项目推广以来.
我们不是将数据作为惰性流处理,而是以一个大批量处理,而是通过基于块的严格处理进行抽象,并在读取最后一个块后保证资源的最终化.这是基于iteratee的编程的本质,也是提供非常好的资源约束的编程.
基于iteratee的IO的缺点是它有一个有点笨拙的编程模型(大致类似于基于事件的编程,而不是基于线程的良好控制).在任何编程语言中,它绝对是一种先进的技术.而对于绝大多数编程问题,懒惰IO完全令人满意.但是,如果您要打开许多文件,或者在许多套接字上进行交谈,或者使用许多同时使用的资源,那么迭代(或枚举)方法可能会有意义.
Joh*_*n L 38
Dons提供了一个非常好的答案,但他遗漏了(对我而言)迭代中最引人注目的特性之一:它们使得更容易推理空间管理,因为必须明确保留旧数据.考虑:
average :: [Float] -> Float
average xs = sum xs / length xs
Run Code Online (Sandbox Code Playgroud)
这是一个众所周知的空间泄漏,因为整个列表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'
Run Code Online (Sandbox Code Playgroud)
但是必须为每个流处理器执行此操作有点不方便.有一些概括(Conal Elliott - Beautiful Fold Zipping),但它们似乎没有流行起来.但是,iteratees可以为您提供类似的表达级别.
aveIter = uncurry (/) <$> I.zip I.sum I.length
Run Code Online (Sandbox Code Playgroud)
这不像折叠那样有效,因为列表仍然会多次迭代,但是它是以块的形式收集的,因此可以有效地对旧数据进行垃圾收集.为了打破该属性,有必要显式保留整个输入,例如使用stream2list:
badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
Run Code Online (Sandbox Code Playgroud)
迭代作为编程模型的状态是一项正在进行的工作,但它比一年前要好得多.我们所学的组合子是有用的(例如zip,breakE,enumWith),并且要少一些,其结果是,内置iteratees和组合程序提供持续更表现力.
那就是说,Dons是正确的,他们是一种先进的技术; 我当然不会将它们用于每个I/O问题.
Pet*_*lák 20
更新:最近在哈斯克尔咖啡厅奥列格Kiseljov显示的是unsafeInterleaveST(这是用于ST单子中实现懒IO)是非常不安全的-它打破了方程式的推理.他表明它允许构建bad_ctx :: ((Bool,Bool) -> Bool) -> Bool
这样的
> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
Run Code Online (Sandbox Code Playgroud)
即使==是可交换的.
惰性IO的另一个问题:实际的IO操作可以推迟到为时已晚,例如在文件关闭之后.引自Haskell Wiki - 懒惰IO的问题:
例如,常见的初学者错误是在文件读完之前关闭文件:
Run Code Online (Sandbox Code Playgroud)wrong = do fileData <- withFile "test.txt" ReadMode hGetContents putStr fileData问题是withFile在强制fileData之前关闭句柄.正确的方法是将所有代码传递给withFile:
Run Code Online (Sandbox Code Playgroud)right = withFile "test.txt" ReadMode $ \handle -> do fileData <- hGetContents handle putStr fileData这里,数据在withFile完成之前被消耗.
这通常是意外的,也是一个容易犯的错误.
另请参阅:Lazy I/O问题的三个示例.
Ben*_*ood 17
到目前为止尚未提及的懒惰IO的另一个问题是它具有令人惊讶的行为.在正常的Haskell程序中,有时很难预测程序的每个部分何时被评估,但幸运的是,由于纯度,除非遇到性能问题,否则它无关紧要.当引入惰性IO时,代码的评估顺序实际上会对其含义产生影响,因此您习惯认为无害的更改可能会导致真正的问题.
作为一个例子,这里有一个关于代码看起来合理的问题,但是由于延迟IO而变得更加混乱:withFile与openFile
这些问题并非总是致命的,但这是另一回事,并且是一个足够严重的头痛,我个人避免懒惰的IO,除非事先做好所有工作确实存在问题.