我通常听说生产代码应避免使用惰性I / O。我的问题是,为什么?在玩弄之外使用Lazy I / O可以吗?是什么使替代方法(例如枚举数)更好?
Answers:
惰性IO的问题在于,释放所获取的任何资源在某种程度上都是不可预测的,因为这取决于程序如何使用数据-其“需求模式”。一旦您的程序删除了对该资源的最后一个引用,GC最终将运行并释放该资源。
惰性流是一种非常方便的编程风格。这就是为什么shell管道如此有趣和流行的原因。
但是,如果资源受到限制(例如在高性能方案中,或者期望扩展到机器极限的生产环境中),则依靠GC清理可能是不足的保证。
有时您必须急于释放资源,以提高可伸缩性。
那么,什么是懒惰IO的替代方案,而不是放弃增量处理(反过来又会消耗太多资源)呢?好吧,我们有foldl
基础的处理程序(又名迭代器或枚举器),由Oleg Kiselyov在2000年代后期引入,并在随后的许多基于网络的项目中得到推广。
而不是将数据作为惰性流或成批处理,而是对基于块的严格处理进行了抽象,一旦读取了最后一个块,就保证了资源的最终确定。这是基于Iteratee的编程的本质,并且提供了非常好的资源约束。
基于iteratee的IO的缺点是它的编程模型有些笨拙(大致类似于基于事件的编程,而不是基于线程的漂亮控制)。在任何编程语言中,它绝对是一种先进的技术。对于绝大多数编程问题,惰性IO完全令人满意。但是,如果您要打开许多文件,或在许多套接字上交谈,或以其他方式同时使用许多资源,则iteratee(或枚举器)方法可能有意义。
Dons提供了一个很好的答案,但是他遗漏了(对我而言)迭代对象最引人注目的功能之一:由于必须明确保留旧数据,因此它们使推理空间管理变得更加容易。考虑:
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'
但是必须为每个流处理器执行此操作有点不方便。有一些概括(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
),并且要少一些,其结果是,内置iteratees和组合程序提供持续更表现力。
就是说,唐斯(Don)是正确的,因为他们是一种先进的技术。我当然不会将它们用于每个I / O问题。
更新:最近在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问题的三个示例。
hGetContents
和withFile
是毫无意义的,因为前者会将手柄“伪关闭”状态,并且将处理关闭你(懒洋洋),这样的代码是完全等同于readFile
,甚至openFile
没有hClose
。这基本上是懒惰的I / O是。如果您不使用readFile
,getContents
或者hGetContents
您没有使用惰性I / O。例如line <- withFile "test.txt" ReadMode hGetLine
工作正常。
hGetContents
可以为您处理关闭文件,但是也可以“尽早”关闭它,并有助于确保可预测地释放资源。
到目前为止尚未提及的惰性IO的另一个问题是它的行为令人惊讶。在普通的Haskell程序中,有时可能难以预测何时评估程序的每个部分,但是幸运的是,由于纯度的考虑,除非您遇到性能问题,否则实际上并不重要。引入惰性IO时,代码的评估顺序实际上会影响其含义,因此习惯于认为无害的更改可能会导致真正的问题。
例如,这是一个关于代码的问题,它看起来合理,但由于延迟的IO而变得更加混乱:withFile与openFile
这些问题并非总是致命的,但这是又一回事,而且头痛得非常严重,我个人避免懒惰的IO,除非预先进行所有工作确实存在问题。