什么是pipe道/pipe道试图解决
我已经看到人们推荐各种惰性IO相关任务的pipe道/导pipe库。 这些库完全解决什么问题?
另外,当我尝试使用一些与哈克相关的库时,很可能有三个不同的版本。 例:
- attoparsec
- pipe道-attoparsec
- attoparsec-pipe道
这使我困惑。 对于我的parsing任务,我应该使用attoparsec或pipes-attoparsec / attoparsec-conduit? pipe道/导pipe版本与普通香草相比有什么好处?
懒惰的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'
立即返回,只有当该强制执行时才会执行IO
操作 。
这是一个危险的部分:通过使用unsafeInterleaveIO
我们已经将一堆IO
操作延迟到了将来的非确定性点,这取决于我们如何使用我们的ByteString
块。
用协程来解决这个问题
我们想要做的是在readChunk
的调用和readChunk
的recursion之间滑动一个块处理步骤。
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
完全加载到内存的情况下增量地完成更多的工作。 不幸的是,这并不是很好的组合 – 我们需要构build我们的消费action
,并将其传递给我们的ByteString
生产者,以使其运行。
基于pipe道的IO
这实质上是pipes
解决的问题 – 它使我们能够轻松地组成有效的协同程序。 例如,我们现在将我们的文件读取器编写为一个Producer
,当文件的效果最终运行时,它可以被认为是“stream”文件的块。
produceFile :: FilePath -> Producer ByteString IO () produceFile fp = produce 0 where produce n = do (n', chunk) <- liftIO (readChunk fp n) yield chunk produce n'
注意上面的代码和readFileCo
之间的相似之处 – 我们简单地将调用replace为协同操作,同时生成我们迄今为止生成的chunk
。 这个调用yield
一个Producer
types,而不是一个原始的IO
操作,我们可以用其他Pipe
types来构build一个名为Effect IO ()
的好的消费pipe道。
所有这些pipe道build设都是静态地完成的,而没有实际调用任何IO
操作。 这就是pipes
如何让你更容易地编写你的协同程序。 当我们在我们的main
IO
操作中调用runEffect
时,所有的效果立即被触发。
runEffect :: Effect IO () -> IO ()
Attoparsec
那你为什么要把attoparsec
插入pipes
? 那么, attoparsec
是懒惰的parsing优化。 如果你正在有效地生产大量的parsing器,那么你将陷入僵局。 你可以
- 使用严格的IO并将整个string加载到内存中,以便与parsing器一起使用。 这很简单,可预测,但效率低下。
- 使用懒惰的IO,并失去推断生产IO效果何时实际运行的能力,从而导致可能的资源泄漏或根据parsing的项目的消耗计划closures处理exception。 这比(1)更有效,但很容易变得不可预测; 要么,
- 使用
pipes
(或conduit
)build立一个协程系统,其中包括懒惰的attoparsec
分析器,允许它在尽可能less的input时进行操作,同时尽可能attoparsec
整个stream中的分析值。
如果你想使用attoparsec,使用attoparsec
对于我的parsing任务,我应该使用attoparsec或pipes-attoparsec / attoparsec-conduit?
两个pipes-attoparsec
和attoparsec-conduit
将给定的attoparsec
Parser
转换为sink / conduit或pipe。 因此你必须使用attoparsec
。
pipe道/导pipe版本与普通香草相比有什么好处?
他们与pipe道和pipe道工作,香草之一不会(至less不是开箱即可)。
如果您不使用pipe道或pipe道,并且对懒惰IO的当前性能感到满意,则不需要更改当前的stream程,尤其是在不编写大型应用程序或处理大型文件的情况下。 你可以简单地使用attoparsec
。
但是,这假定您知道惰性IO的缺点。
懒惰的IO有什么问题? (用文件学习问题)
让我们不要忘记你的第一个问题:
这些库完全解决什么问题?
他们解决了stream式数据问题(参见1和3 ),这些问题发生在具有惰性IO的函数式语言中。 懒惰的IO有时会给你不是你想要的(见下面的例子),有时很难确定一个特定的懒惰操作所需的实际系统资源(数据读/写块/字节/缓冲/ onclose / onopen …) 。
过度懒惰的例子
import System.IO main = withFile "myfile" ReadMode hGetContents >>= return . (take 5) >>= putStrLn
这将不会打印任何内容,因为数据的评估发生在putStrLn
,但是此时句柄已经closures。
用有毒的酸固定火
虽然下面的代码片段解决了这个问题,但它还有另一个令人讨厌的特性:
main = withFile "myfile" ReadMode $ \handle -> hGetContents handle >>= return . (take 5) >>= putStrLn
在这种情况下, hGetContents
将读取所有的文件 ,一开始你并不期待。 如果你只是想检查一个可能有几GB的文件的魔术字节,这是不是要走的路。
正确使用withFile
显然,解决scheme是take
withFile
上下文中的东西:
main = withFile "myfile" ReadMode $ \handle -> fmap (take 5) (hGetContents handle) >>= putStrLn
顺便说一句,也是pipe道作者提到的解决scheme:
这个[..]回答了一个问题,人们有时会问我关于
pipes
,如果资源pipe理不是
pipes
的核心重点,为什么我应该使用pipes
而不是惰性IO?许多提出这个问题的人通过Oleg发现了stream编程,他在资源pipe理方面构build了惰性IO问题。 但是,我从来没有发现这个论点是孤立的, 您可以简单地通过将资源获取从惰性IO中分离来解决大多数资源pipe理问题,如下所示:[请参阅上面的最后一个示例]
这使我们回到以前的陈述:
你可以简单地使用
attoparsec
[懒惰的IO,假设]你知道惰性IO的缺点。
参考
- Iteratee I / O ,它更好的解释了这个例子,并提供了一个更好的概述
- Gabriel Gonzalez(pipe道维护者/作者): 关于stream编程的推理
- Michael Snoyman(pipe道维护者/作者): Conduit与Enumerator
这是一个伟大的播客与两个库的作者:
http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/
它会回答你的大部分问题。
总之,这两个库都处理stream处理的问题,这在处理IO时非常重要。 本质上,它们pipe理块中的数据传输,从而允许您在服务器和客户端上传输1GB的文件,只需要64KB的RAM。 没有stream媒体,你将不得不在两端分配尽可能多的内存。
这些库的一个较老的替代品是懒惰的IO,但它充满了问题,并使应用程序容易出错。 这些问题在播客中讨论。
关于使用哪一个图书馆,这更多的是品味的问题。 我更喜欢“pipe道”。 详细的差异也在播客中讨论。