Если вы просто хотите код, это:
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)
В любом случае, при наличии доступного перечислителя, его легко составить с перечислителем 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
являются ярлыками для простого перечисления по файлу и запуска получающегося итерируемого (к сожалению, порядок аргументов переключен с enumFile):
procFile2 iFile oFile = fileDriver (joinI $ enumStripLines $ writer oFile) iFile
Приложение: вот ситуация, когда вам понадобится дополнительная сила convStream. Предположим, вы хотите объединить каждые 2 строки в одну. Вы не можете использовать 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
также допускают монадические действия, поскольку итерируемая обработка потока является преобразователем монад.