haskell 懒惰I/O有什么不好?

s1ag04yj  于 2022-11-14  发布在  其他
关注(0)|答案(6)|浏览(184)

我通常听说生产代码应该避免使用Lazy I/O。我的问题是,为什么?除了玩玩之外,使用Lazy I/O是否合适?是什么使替代方法(例如枚举数)更好?

5sxhfpxr

5sxhfpxr1#

Lazy IO的问题是,释放所获得的任何资源都是不可预测的,因为这取决于程序使用数据的方式--它的“需求模式”。一旦程序删除了对资源的最后一个引用,GC最终将运行并释放该资源。
懒惰流是一种非常方便的编程风格。这就是为什么shell管道如此有趣和流行的原因。
但是,如果资源受到限制(例如在高性能场景中,或者在期望扩展到计算机限制的生产环境中),则依赖GC来进行清理可能是不充分的保证。
有时候,您必须急切地释放资源,以提高可伸缩性。
那么,有什么替代方法可以替代惰性IO,而不意味着放弃增量处理(这反过来会消耗太多的资源)呢?我们有基于foldl的处理,也称为迭代器或枚举器,由**Oleg Kiselyov in the late 2000s**引入,并在许多基于网络的项目中普及。
我们不是将数据处理为懒惰的流,或者在一个大的批处理中,而是在基于块的严格处理上进行抽象,一旦读取了最后一个块,就可以保证资源的最终化。这是基于迭代的编程的本质,也是一个提供非常好的资源约束的编程。
基于迭代的IO的缺点是,它的编程模型有些笨拙(大致类似于基于事件的编程,而不是基于线程的控制)。在任何编程语言中,它都绝对是一种高级技术。对于绝大多数编程问题,lazy IO完全令人满意。但是,如果您要打开许多文件,或者在许多套接字上进行对话,或者使用许多同时的资源,迭代对象(或枚举器)方法可能是有意义的。

wwwo4jvm

wwwo4jvm2#

唐斯给出了一个很好的答案,但是他忽略了迭代者(对我来说)最引人注目的特征之一:它们使空间管理更容易推理,因为旧数据必须显式保留。

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

这是一个众所周知的空间泄漏,因为整个列表xs必须保留在内存中,以便计算sumlength。可以通过创建一个fold来提高使用者的效率:

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

这不如fold有效,因为列表仍然会迭代多次,但是它是以块的形式收集的,所以旧数据可以被有效地垃圾收集。为了打破这个属性,有必要显式地保留整个输入,比如使用stream 2list:

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

迭代对象作为一个编程模型的状态是一个正在进行中的工作,但是它比一年前要好得多。我们正在学习哪些组合子是有用的(例如zipbreakEenumWith),哪些是不太有用的,结果是内置的迭代对象和组合子提供了不断增加的表达能力。
也就是说,唐斯是正确的,他们是一个先进的技术;我当然不会对每个I/O问题都使用它们。

ckocjqey

ckocjqey3#

我一直在生产代码中使用lazy I/O。就像Don提到的,这只是在某些情况下的一个问题。但是对于仅仅阅读几个文件来说,它工作得很好。

cgh8pdjw

cgh8pdjw4#

**更新:**最近在haskell-cafe Oleg Kiseljov showed上,unsafeInterleaveST(用于在ST monad中实现lazy 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完成之前被使用。
这通常是意料之外的,也是一个容易犯的错误。
另请参阅:Three examples of problems with Lazy I/O

vsnjm48y

vsnjm48y5#

惰性IO的另一个问题还没有被提及,那就是它有着令人惊讶的行为。在一个普通的Haskell程序中,有时候很难预测程序的每个部分何时被求值,但幸运的是,由于纯粹性,除非你有性能问题,否则它真的不重要。当引入惰性IO时,代码的求值顺序实际上会影响它的意义。因此,那些您认为无害的更改可能会给您带来真正的问题。
例如,下面是一个关于代码的问题,该代码看起来合理,但由于延迟IO而变得更加混乱:withFile vs. openFile
这些问题并不总是致命的,但这是另一件需要考虑的事情,而且是一个非常严重的头痛问题,我个人避免使用懒惰IO,除非预先做所有的工作真实的有问题。

mccptt67

mccptt676#

懒惰I/O的缺点是,作为程序员,你必须对某些资源进行微观管理,而不是对实现进行管理。例如,下面哪一项是“不同的”?

  • freeSTRef :: STRef s a -> ST s ()
  • closeIORef :: IORef a -> IO ()
  • endMVar :: MVar a -> IO ()
  • discardTVar :: TVar -> STM ()
  • hClose :: Handle -> IO ()
  • finalizeForeignPtr :: ForeignPtr a -> IO ()

......在所有这些 * 轻蔑 * 的定义中,最后两个-hClosefinalizeForeignPtr-实际上是存在的。至于其余的,它们在语言中能提供什么服务,由实现来执行要可靠得多!
因此,如果文件句柄和外部引用等资源的释放也留给实现,那么 lazy I/O可能不会比 lazy 求值更糟糕。

相关问题