Как сделать канал, подобный takeWhile, но принимающий максимум определенное количество байтов?

Я пытаюсь создать канал, нечто среднее между takeWhile и isolate. То есть он будет потреблять из ввода и уступать выходу до тех пор, пока либо предикат больше не будет выполняться, либо он не достигнет предела байтов. Я знаю, что подпись типа будет

isolateWhile :: (Monad m) => Int -> (Word8 -> Bool) -> Conduit ByteString m ByteString

Как пример его использования:

{-# LANGUAGE OverloadedStrings #-}
import Data.Conduit
import qualified Data.Conduit.List   as CL
import qualified Data.Conduit.Binary as CB
import Control.Monad.Trans.Class

charToWord = fromIntegral . fromEnum

example :: Int -> Char -> IO ()
example limit upTo = do
    untaken <- CB.sourceLbs "Hello, world!" $= conduit $$ CB.sinkLbs
    putStrLn $ "Left " ++ show untaken
  where
    conduit = do
      taken <- toConsumer $ isolateWhile limit (/= charToWord upTo) =$ CB.sinkLbs
      lift $ putStrLn $ "Took " ++ show taken
      CL.map id  -- pass the rest through untouched

я ожидаю, что

ghci> example 5 'l'
Took "He"
Left "llo, world!"
ghci> example 5 'w'
Took "Hello"
Left ", world!"

Однако самое простое возможное определение isolateWhile:

isolateWhile limit pred = CB.isolate limit =$= CB.takeWhile pred

урожаи

ghci> example 5 'l'
Took "He"
Left ", world!"
ghci> example 5 'w'
Took "Hello"
Left ", world!"

Другими словами, isolate съест все Hello, оставив He до takeWhile и отбросив llo. Эта потеря данных нежелательна для моего приложения. Однако следует отметить, что второй случай дает ожидаемый результат.

Если я поменяю местами операнды =$= так:

isolateWhile limit pred = CB.takeWhile pred =$= CB.isolate limit

Затем

ghci> example 5 'l'
Took "He"
Left ", world!"
ghci> example 5 'w'
Took "Hello"
Left ""

Теперь я исправил первый тест, но сломал второй! На этот раз takeWhile возьмет все, что ему нужно, а isolate возьмет подмножество этого; но все, что takeWhile использует, что isolate не использует, будет отброшено, а это нежелательно.

Наконец, я попробовал:

isolateWhile limit pred = do
  untaken <- CB.isolate limit =$= (CB.takeWhile pred >> CL.consume)
  mapM_ leftover $ reverse untaken

Это действительно работает! Все, что isolate принимает, а takeWhile нет, потребляется CL.consume и помещается обратно в поток с помощью leftover. К сожалению, это выглядит как ужасный кладж, и нежелательно (хотя и не неприменимо) он будет буферизовать до limit байт в памяти только для того, чтобы вернуть его обратно с leftover. Это похоже на пустую трату.

Единственное решение, которое я могу придумать, это написать его в терминах примитивов await, yield и leftover как takeWhile и isolate сами написали. Хотя это решило бы все проблемы без больших потерь, похоже, должен быть лучший способ.

Я что-то упустил, или действительно нет лучшего способа написать это?


person icktoofay    schedule 20.10.2013    source источник


Ответы (1)


В текущей версии conduit есть известное ограничение: fusion всегда отбрасывает оставшиеся ниже по течению остатки, и это именно то, с чем вы здесь сталкиваетесь. Сейчас ведутся некоторые дискуссии об архитектуре для решения этой проблемы, но на данный момент написание вашей функции в терминах примитивов, вероятно, является лучшим вариантом.

person Michael Snoyman    schedule 21.10.2013
comment
Спасибо. Я беспокоился, что это так. Пока я написал это с точки зрения примитивов, но мне было бы интересно посмотреть, как это будет работать, когда будет достигнуто разрешение. - person icktoofay; 22.10.2013