50

我见过有人为各种惰性 IO 相关任务推荐管道/导管库。这些库究竟解决了什么问题?

此外,当我尝试使用一些与 hackage 相关的库时,很可能存在三个不同的版本。例子:

这让我很困惑。对于我的解析任务,我应该使用 attoparsec 还是 pipe-attoparsec/attoparsec-conduit?与普通的 attoparsec 相比,管道/导管版本给我带来了什么好处?

4

3 回答 3

63

懒惰的 IO

懒惰的 IO 是这样工作的

readFile :: FilePath -> IO ByteString

whereByteString保证只能逐块读取。为此,我们可以(几乎)写

-- given `readChunk` which reads a chunk beginning at n
readChunk :: FilePath -> Int -> IO (Int, ByteString)

readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- readChunks n'
    return (chunk <> chunks)

但在这里我们注意到,IO 操作readChunks n'是在返回之前执行的,甚至是可用的部分结果chunk。这意味着我们一点也不懒惰。为了解决这个问题,我们使用unsafeInterleaveIO

readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- unsafeInterleaveIO (readChunks n')
    return (chunk <> chunks)

这会导致readChunks n'立即返回,只有在强制执行该重击时才会执行一个IO动作。

这是危险的部分:通过使用unsafeInterleaveIO,我们将一堆IO动作延迟到未来的不确定点,这取决于我们如何使用我们的ByteString.

修复协程问题

我们想做的是在调用readChunk和递归之间滑动一个块处理步骤readChunks

readFileCo :: Monoid a => FilePath -> (ByteString -> IO a) -> IO a
readFileCo fp action = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    a           <- action chunk
    as          <- readChunks n'
    return (a <> as)

现在我们有机会在IO加载每个小块后执行任意操作。ByteString这让我们可以在不完全加载到内存的情况下增量地做更多的工作。不幸的是,它的组合性并不好——我们需要构建我们的消费action并将其传递给我们的ByteString生产者才能运行。

基于管道的 IO

这基本上就是pipes解决问题的方法——它使我们能够轻松地编写有效的协同程序。例如,我们现在将文件阅读器编写为 a Producer,当它的效果最终运行时,它可以被认为是“流式传输”文件的块。

produceFile :: FilePath -> Producer ByteString IO ()
produceFile fp = produce 0 where
  produce n = do
    (n', chunk) <- liftIO (readChunk fp n)
    yield chunk
    produce n'

注意这段代码和上面代码的相似之处readFileCo——我们只是简单地将协程动作的调用替换为我们目前生成的yielding 。chunk这个调用yield构建了一个Producer类型而不是原始IO操作,我们可以将其与其他Pipes 类型组合,以构建一个称为 an 的良好消费管道Effect IO ()

所有这些管道构建都是静态完成的,而无需实际调用任何IO操作。这就是pipes让您更轻松地编写协程的方法。当我们调用runEffect我们的main IO动作时,所有的效果都会立即触发。

runEffect :: Effect IO () -> IO ()

阿托帕秒

那么你为什么要插入attoparsecpipes?好吧,attoparsec针对惰性解析进行了优化。如果您以有效的方式生成提供给attoparsec解析器的块,那么您将陷入僵局。你可以

  1. 使用严格的 IO 并将整个字符串加载到内存中,只是为了通过解析器懒惰地使用它。这是简单的、可预测的,但效率低下。
  2. 使用惰性 IO 并失去推断生产 IO 效果何时实际运行的能力,从而导致可能的资源泄漏或根据已解析项目的消耗计划关闭句柄异常。这比(1)更有效,但很容易变得不可预测;或者,
  3. 使用pipes(或conduit)构建一个协程系统,其中包括您的惰性attoparsec解析器,允许它在尽可能少的输入上进行操作,同时在整个流中尽可能惰性地生成解析值。
于 2014-03-30T16:35:28.917 回答
18

如果要使用 attoparsec,请使用 attoparsec

对于我的解析任务,我应该使用 attoparsec 还是 pipe-attoparsec/attoparsec-conduit?

两者都pipes-attoparsec并将attoparsec-conduit给定attoparsec Parser的转换为水槽/导管或管道。因此,您必须使用attoparsec任何一种方式。

与普通的 attoparsec 相比,管道/导管版本给我带来了什么好处?

他们使用管道和导管,而香草则不会(至少不是开箱即用的)。

如果您不使用管道或管道,并且您对惰性 IO 的当前性能感到满意,则无需更改当前流程,尤其是在您不编写大型应用程序或处理大型文件时。您可以简单地使用attoparsec.

但是,这假设您知道惰性 IO 的缺点。

懒惰的IO是怎么回事?(问题研究withFile

别忘了你的第一个问题:

这些库究竟解决了什么问题?

它们解决了流数据问题(参见13),该问题发生在具有惰性 IO 的函数式语言中。惰性 IO 有时给你的不是你想要的(见下面的例子),有时很难确定一个特定的惰性操作所需的实际系统资源(是以块/字节/缓冲/onclose/onopen…的形式读取/写入的数据) .

过度懒惰的例子

import System.IO
main = withFile "myfile" ReadMode hGetContents
       >>= return . (take 5)
       >>= putStrLn

这不会打印任何内容,因为数据的评估发生在 中putStrLn,但此时句柄已经关闭。

用毒酸灭火

虽然以下代码段解决了这个问题,但它还有另一个令人讨厌的功能:

main = withFile "myfile" ReadMode $ \handle -> 
           hGetContents handle
       >>= return . (take 5)
       >>= putStrLn

在这种情况下,hGetContents将读取所有文件,这是您一开始没想到的。如果您只想检查可能有几 GB 大小的文件的魔术字节,那么这不是要走的路。

withFile正确使用

显然,解决方案是针对上下文take中的事物:withFile

main = withFile "myfile" ReadMode $ \handle -> 
           fmap (take 5) (hGetContents handle)
       >>= putStrLn

顺便说一句,也是管道作者提到的解决方案:

这 [..] 回答了人们有时会问我的一个问题pipes,我将在这里过渡:

如果资源管理不是核心关注点pipes,为什么要使用pipes惰性 IO 代替呢?

很多问这个问题的人都是通过 Oleg 发现流式编程的,Oleg 将惰性 IO 问题从资源管理的角度提出来。然而,我从来没有发现这个论点孤立地令人信服。您可以通过将资源获取与惰性 IO 分开来解决大多数资源管理问题,如下所示:[参见上面的最后一个示例]

这让我们回到我之前的陈述:

您可以简单地使用attoparsec[...] [与惰性 IO,假设] 您知道惰性 IO 的缺点。

参考

于 2014-03-30T09:22:54.810 回答
12

这是两个库的作者的精彩播客:

http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/

它会回答你的大部分问题。


简而言之,这两个库都解决了流式传输问题,这在处理 IO 时非常重要。从本质上讲,它们管理数据块的传输,因此允许您传输一个 1GB 的文件,在服务器和客户端上仅占用 64KB 的 RAM。如果没有流式传输,您将不得不在两端分配尽可能多的内存。

这些库的一个较旧的替代方案是惰性 IO,但它充满了问题并使应用程序容易出错。播客中讨论了这些问题。

关于使用哪一个库,这更多是一个品味问题。我更喜欢“管道”。播客中也讨论了详细的差异。

于 2014-03-30T09:22:22.617 回答