什么是管道/管道试图解决

时间:2014-03-30 08:44:11

标签: haskell pipe conduit haskell-pipes

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

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

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

3 个答案:

答案 0 :(得分:60)

懒惰IO

懒惰的IO就像这样

readFile :: FilePath -> IO ByteString

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

-- 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'立即返回,只有当强制执行thunk时才会执行 IO操作

这是危险的部分:通过使用unsafeInterleaveIO我们将一堆IO行动延迟到未来的非确定性点,这取决于我们如何消耗ByteString的块

解决协同程序的问题

我们要做的是在调用readChunkreadChunks上的递归之间滑动一个块处理步骤。

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' action
    return (a <> as)

现在我们有机会在每个小块加载后执行任意IO个动作。这使我们可以逐步完成更多工作,而无需将ByteString完全加载到内存中。不幸的是,它并不是非常复杂的 - 我们需要构建我们的消费action并将其传递给我们的ByteString生产者以使其运行。

基于管道的IO

这基本上是pipes解决的问题 - 它允许我们轻松地组成有效的协同例程。例如,我们现在将文件阅读器编写为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之间的相似之处 - 我们只是使用yield我们迄今为止生成的chunk来替换对coroutine操作的调用。对yield的此调用会构建Producer类型而不是原始IO操作,我们可以将这些操作与其他Pipe类型组合,以构建一个名为{{{}的良好消费管道{1}}。

所有这些管道构建都是静态完成的,而不实际调用任何Effect IO ()个动作。这就是IO让您更轻松地编写协同程序的方式。当我们在pipes runEffect行动中致电main时,会立即触发所有效果。

IO

Attoparsec

那你为什么要把runEffect :: Effect IO () -> IO () 插入attoparsec?好吧,pipes针对延迟解析进行了优化。如果你以有效的方式生成以attoparsec解析器提供的块,那么你将陷入僵局。你可以

  1. 使用严格的IO并将整个字符串加载到内存中,以便与解析器一起使用它。这很简单,可预测,但效率低下。
  2. 使用惰性IO并且无法根据已解析项目的消耗计划来推断生产IO效果何时实际运行导致可能的资源泄漏或关闭句柄异常。这比(1)更有效,但很容易变得不可预测;或者,
  3. 使用attoparsec(或pipes)构建一个协程系统,其中包含您的惰性conduit解析器,允许它在生成解析值时以尽可能少的输入进行操作尽可能懒散地穿过整个溪流。

答案 1 :(得分:18)

如果要使用attoparsec,请使用attoparsec

  

对于我的解析任务,我应该使用attoparsec或pipes-attoparsec / attoparsec-conduit吗?

pipes-attoparsecattoparsec-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

顺便提一句,这也是解决方案mentioned by the author of pipes

  

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

     
    

如果资源管理不是pipes的核心重点,为什么我应该使用pipes而不是懒惰的IO?

  
     

许多提出这个问题的人通过Oleg发现了流编程,Oleg在资源管理方面构建了懒惰的IO问题。但是,我从来没有发现这个论点孤立地引人注目;您可以简单地通过将资源获取与惰性IO分离来解决大多数资源管理问题,如下所示:[参见上面的示例]

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

  

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

参考

答案 2 :(得分:13)

这里是两个图书馆作者的精彩播客:

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

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


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

这些库的旧替代品是惰性IO,但它充满问题并使应用程序容易出错。这些问题将在播客中讨论。

关于使用哪一个图书馆,这更多的是品味问题。我更喜欢&#34;管道&#34;。播客也讨论了详细的差异。