Jeśli chcesz tylko kod, to w ten sposób:
procFile' iFile oFile = fileDriver (joinI $
enumLinesBS ><>
mapChunks (map rstrip) $
I.mapM_ (B.appendFile oFile))
iFile
Komentarz:
Jest to proces trzyetapowy: najpierw przekształcić surowy strumień do strumienia linii, a następnie zastosować funkcja do konwersji tego strumienia linii, a na końcu zużyjesz strumień. Ponieważ rstrip
znajduje się w środkowej fazie, stworzy transformator strumieniowy (Enumeratee).
Można użyć albo mapChunks
lub convStream
, ale mapChunks
jest prostszy. Różnica polega na tym, że mapChunks
nie pozwala na przekraczanie granic fragmentów, podczas gdy convStream
jest bardziej ogólny. Wolę convStream
, ponieważ nie ujawnia żadnej z podstawowych realizacji, ale jeśli mapChunks
jest wystarczająca, wynikowy kod jest zwykle krótszy.
rstripE :: Monad m => Enumeratee [ByteString] [ByteString] m a
rstripE = mapChunks (map rstrip)
uwaga dodatkowa map
w rstripE
. Strumień zewnętrzny (który jest wejściem do rstrip) ma typ [ByteString]
, więc musimy na niego mapować rstrip
.
Dla porównania, to co by to wyglądało, gdyby były realizowane z 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
to dłużej i jest mniej efektywne, ponieważ będzie ona miała zastosowanie funkcji rstrip tylko na jednej linii na raz, nawet choć może być dostępnych więcej linii. Jest to możliwe, aby działać na wszystkich obecnie dostępnych fragmencie, który jest bliżej do wersji mapChunks
:
rstripE'2 :: Enumeratee [ByteString] [ByteString] m a
rstripE'2 = convStream (liftM (map rstrip) getChunk)
W każdym razie, z enumeratee odpędzania dostępnej, to łatwo składa się z enumLinesBS
enumeratee:
enumStripLines :: Monad m => Enumeratee ByteString [ByteString] m a
enumStripLines = enumLinesBS ><> rstripE
Operator kompozycji ><>
działa w tej samej kolejności, co operator strzałki >>>
. enumLinesBS
dzieli strumień na linie, a następnie rstripE
je rozbiera.Teraz wystarczy dodać Konsumentów (co jest normalne iteratee) i skończysz:
writer :: FilePath -> Iteratee [ByteString] IO()
writer fp = I.mapM_ (B.appendFile fp)
processFile iFile oFile =
enumFile defaultBufSize iFile (joinI $ enumStripLines $ writer oFile) >>= run
W fileDriver
funkcje są skróty dla prostu wyliczanie nad plikiem i działa wynikowy iteratee (niestety kolejność argumentów jest przełączany z enumFile):
procFile2 iFile oFile = fileDriver (joinI $ enumStripLines $ writer oFile) iFile
Uzupełnienie: tu jest sytuacja, w której trzeba będzie dodatkową moc convStream. Załóżmy, że chcesz połączyć co dwie linie w jedną. Nie można użyć mapChunks
. Rozważ, gdy porcja jest elementem singleton, [bytestring]
. mapChunks
nie zapewnia żadnego sposobu uzyskania dostępu do następnej porcji, więc nie ma nic innego, jak się z tym wiązać. Z convStream
Jednak to proste:
concatPairs = convStream $ do
line1 <- I.head
line2 <- I.head
return $ line1 `B.append` line2
ten wygląda jeszcze ładniej w aplikacyjnej stylu
convStream $ B.append <$> I.head <*> I.head
Można myśleć o convStream
jako stale pochłania część strumienia za pomocą dostarczonego iteratee, a następnie wysyłania przekształcona wersja dla wewnętrznego klienta. Czasem nawet to nie jest wystarczająco ogólne, ponieważ ten sam iteratee jest wywoływany na każdym etapie. W takim przypadku można użyć parametru unfoldConvStream
, aby przekazać stan między kolejnymi iteracjami.
convStream
i unfoldConvStream
pozwalają również na monadyczne akcje, ponieważ przetwarzanie strumienia iteratee to transformator monadowy.
John, dziękuję za tę niezwykle szczegółową odpowiedź! Dokładnie tego potrzebowałem. –
Dwie małe uwagi: typ rstripE wymaga kwalifikatora typograficznego (Monad m) =>, a moja funkcja rstrip musi skleić znak końca linii, aby zintegrować się z enumLinesBS. W przeciwnym razie działa jak urok! –
Dzięki za wskazanie tego, dodałem kontekst klasy typów. –