如果你只想要代码,那就是:
procFile' iFile oFile = fileDriver (joinI $
enumLinesBS ><>
mapChunks (map rstrip) $
I.mapM_ (B.appendFile oFile))
iFile
评论:
这是一个三阶段的过程:首先将原始流转换为行流,然后应用函数转换行流,最后消费流。由于rstrip处于中间阶段,它将创建一个流转换器(Enumeratee)。
您可以使用mapChunks 或convStream,但mapChunks 更简单。不同之处在于mapChunks 不允许您跨越块边界,而convStream 更通用。我更喜欢convStream,因为它不会暴露任何底层实现,但如果mapChunks 足够,那么生成的代码通常会更短。
rstripE :: Monad m => Enumeratee [ByteString] [ByteString] m a
rstripE = mapChunks (map rstrip)
注意rstripE 中额外的map。外部流(rstrip 的输入)的类型为[ByteString],因此我们需要将rstrip 映射到它上面。
作为比较,如果使用 convStream 实现,效果如下:
rstripE' :: Enumeratee [ByteString] [ByteString] m a
rstripE' = convStream $ do
mLine <- I.peek
maybe (return B.empty) (\line -> I.drop 1 >> return (rstrip line)) mLine
这更长,而且效率更低,因为它一次只会将 rstrip 函数应用于一行,即使可能有更多行可用。可以处理所有当前可用的块,更接近mapChunks 版本:
rstripE'2 :: Enumeratee [ByteString] [ByteString] m a
rstripE'2 = convStream (liftM (map rstrip) getChunk)
不管怎样,有了剥离枚举,它很容易用enumLinesBS枚举:
enumStripLines :: Monad m => Enumeratee ByteString [ByteString] m a
enumStripLines = enumLinesBS ><> rstripE
组合运算符><> 遵循与箭头运算符>>> 相同的顺序。 enumLinesBS 将流分成几行,然后rstripE 剥离它们。现在你只需要添加一个消费者(这是一个普通的迭代者),你就完成了:
writer :: FilePath -> Iteratee [ByteString] IO ()
writer fp = I.mapM_ (B.appendFile fp)
processFile iFile oFile =
enumFile defaultBufSize iFile (joinI $ enumStripLines $ writer oFile) >>= run
fileDriver 函数是简单地枚举文件并运行生成的 iteratee 的快捷方式(不幸的是,参数顺序是从 enumFile 切换的):
procFile2 iFile oFile = fileDriver (joinI $ enumStripLines $ writer oFile) iFile
附录:在这种情况下,您需要 convStream 的额外功能。假设您想将每 2 行连接成 1 行。你不能使用mapChunks。考虑当块是单例元素时,[bytestring]。 mapChunks 没有提供任何访问下一个块的方法,所以没有其他东西可以与它连接。但是,使用 convStream 很简单:
concatPairs = convStream $ do
line1 <- I.head
line2 <- I.head
return $ line1 `B.append` line2
这在应用风格上看起来更好,
convStream $ B.append <$> I.head <*> I.head
您可以将convStream 视为使用提供的迭代器不断消耗流的一部分,然后将转换后的版本发送给内部消费者。有时即使这样也不够通用,因为在每一步都会调用同一个迭代对象。在这种情况下,您可以使用unfoldConvStream 在连续迭代之间传递状态。
convStream 和 unfoldConvStream 也允许单子操作,因为流处理迭代器是单子转换器。