2011-07-10 10 views
19

Próbuję zrozumieć, jak używać biblioteki iteratee z Haskellem. Wszystkie artykuły, które do tej pory widziałem, skupiają się na budowaniu intuicji na temat tego, jak można zbudować iteracje, co jest pomocne, ale teraz, gdy chcę zejść i faktycznie z nich korzystać, czuję się trochę na morzu. Patrzenie na kod źródłowy iteratees ma dla mnie ograniczoną wartość.Haskell iteratee: prosty działający przykład usuwania spływu białych znaków

Powiedzmy mam tę funkcję, która trymuje końcowe białe znaki z linii:

import Data.ByteString.Char8 

rstrip :: ByteString -> ByteString 
rstrip = fst . spanEnd isSpace 

Co chciałbym zrobić, to: make to pod w iteratee, odczytać plik i zapisać go gdzieś indziej z końcowa spacja usunięta z każdej linii. W jaki sposób mam zamiar zorganizować to za pomocą iteracji? Widzę, że istnieje funkcja enumLinesBS w Data.Iteratee.Char, do której mógłbym się zgłosić, ale nie wiem, czy powinienem użyć mapChunks lub convStream lub jak przepakować powyższą funkcję w iteratee.

Odpowiedz

16

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.

+0

John, dziękuję za tę niezwykle szczegółową odpowiedź! Dokładnie tego potrzebowałem. –

+0

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! –

+0

Dzięki za wskazanie tego, dodałem kontekst klasy typów. –

Powiązane problemy