Ответ 1
Если вы просто хотите код, то это:
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)
Обратите внимание на дополнительный map
в rstripE
. Внешний поток (который является входом в 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)
Во всяком случае, с доступным списком enumeratee, он легко состоит из enumLinesBS
enumeratee:
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. Предположим, вы хотите объединить каждые две строки в одну. Вы не можете использовать 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
также допускают монодичные действия, поскольку итерация обработки потока является монадным трансформатором.