Hastell iteratee: простой проработанный пример удаления конечных пробелов - PullRequest
19 голосов
/ 11 июля 2011

Я пытаюсь понять, как использовать библиотеку iteratee с Haskell.Кажется, что все статьи, которые я видел до сих пор, сосредоточены на построении интуиции о том, как могут быть построены итераторы, что полезно, но теперь, когда я хочу опускаться и фактически использовать их, я чувствую себя немного в море.Просмотр исходного кода для итераторов имеет для меня ограниченную ценность.

Допустим, у меня есть эта функция, которая обрезает завершающий пробел из строки:

import Data.ByteString.Char8

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

Что бы я хотелdo is: превратить это в итератора, прочитать файл и записать его где-нибудь в другом месте с конечным пробелом, убранным из каждой строки.Как бы я пошел на структурирование этого с итераторами?Я вижу, что в Data.Iteratee.Char есть функция enumLinesBS, с которой я мог бы в этом разобраться, но я не знаю, следует ли мне использовать mapChunks или convStream или как упаковать вышеупомянутую функцию в итератор.

1 Ответ

16 голосов
/ 11 июля 2011

Если вы просто хотите код, это:

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 также допускают монадические действия, поскольку итерируемая обработка потока является преобразователем монад.

...