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

Sib*_*ibi 49 haskell pipe conduit haskell-pipes

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

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

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

J. *_*son 61

懒惰的IO

懒惰的IO就是这样的

readFile :: FilePath -> IO ByteString
Run Code Online (Sandbox Code Playgroud)

哪里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)
Run Code Online (Sandbox Code Playgroud)

但是在这里我们注意到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)
Run Code Online (Sandbox Code Playgroud)

导致readChunks n'立即返回,仅在强制执行thunk时才执行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)
Run Code Online (Sandbox Code Playgroud)

现在我们有机会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'
Run Code Online (Sandbox Code Playgroud)

注意:此代码之间的相似之处readFileCo上面我们简单地替换调用协程动作yield荷兰国际集团在chunk我们迄今为止生产的.这个调用yield构建一个Producer类型而不是一个原始IO动作,我们可以用其他Pipe类型组合,以构建一个很好的消费管道,称为Effect IO ().

所有这些管道构建都是静态完成的,而不实际调用任何IO操作.这就是pipes让你更容易编写协同程序的方法.当我们打电话给runEffect我们的main IO行动时,所有效果立即被触发.

runEffect :: Effect IO () -> IO ()
Run Code Online (Sandbox Code Playgroud)

Attoparsec

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

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

  • @Sibi可能中缀`mappend`.Hayoo!可以找到它:http://holumbus.fh-wedel.de/hayoo/hayoo.html?query =%3C%3E (3认同)
  • @Sibi是的.有关此问题的更多探索,请考虑[Joachim Breitner的"如何在Monad中构建列表"](https://www.joachim-breitner.de/blog/archives/620-Constructing-a-list-in-a- Monad.html).特别是,检查他概述的问题(并没有提供明确的解决方案),因为正是那种思想`管道`和`管道`解决. (3认同)
  • 抱歉,惰性 ByteString 的“Monoid”。这个想法应该是惰性字节字符串只是严格字节字符串的惰性列表,因此`(&lt;&gt;)`只不过是这些列表上的`(++)`。 (2认同)
  • 关于attoparsec部分的第1点,严格的IO是什么意思?这是否指的是没有`unsafeInterleaveIO`的原始`readFile`函数,它将整个文件读入内存? (2认同)

Zet*_*eta 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
Run Code Online (Sandbox Code Playgroud)

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

用有毒的酸固定火

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

main = withFile "myfile" ReadMode $ \handle -> 
           hGetContents handle
       >>= return . (take 5)
       >>= putStrLn
Run Code Online (Sandbox Code Playgroud)

在这种情况下,hGetContents将读取所有文件,这是您最初没想到的.如果您只想检查文件的魔术字节大小可能是几GB,那么这不是可行的方法.

withFile正确使用

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

main = withFile "myfile" ReadMode $ \handle -> 
           fmap (take 5) (hGetContents handle)
       >>= putStrLn
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

这让我们回到我先前的声明:

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

参考


Nik*_*kov 13

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

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

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


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

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

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