2014-02-11 3 views
4

Я экспериментировал с новым пакетом pipe-http, и у меня возникла мысль. У меня есть два парсера для веб-страницы, одна из которых возвращает позиции и другой номер из другого места на странице. Когда я хватаю страницу, было бы неплохо объединить эти парсеры вместе и получить их результаты одновременно от одного и того же производителя, вместо того, чтобы извлекать страницу дважды или извлекать весь html в память и анализировать ее дважды.Соедините двух потребителей с одним потребителем, который возвращает несколько значений?

Другими словами, у вас есть два потребителя:

c1 :: Consumer a m r1 
c2 :: Consumer a m r2 

Можно ли сделать такую ​​функцию:

combineConsumers :: Consumer a m r1 -> Consumer a m r2 -> Consumer a m (r1, r2) 
combineConsumers = undefined 

Я пытался несколько вещей, но я не могу выясните это. Я понимаю, если это невозможно, но это было бы удобно.

Edit:

Я сожалею, что это получается, что я делал предположение о трубах-attoparsec, из-за мой опыт с трубопроводом-attoparsec, что заставило меня задать неправильный вопрос. Pipes-attoparsec превращает attoparsec в трубы Parser, когда я только предполагал, что он вернет трубы Consumer. Это означает, что я не могу фактически превратить два парнера attoparsec в потребителей, которые берут текст и возвращают результат, а затем используют их с простой старой экосистемой труб. Извините, но я просто не понимаю, как разбираются трубы.

Несмотря на то, что это не помогает мне, ответ Артура в значительной степени соответствует тому, что я себе представлял, когда задавал этот вопрос, и я, вероятно, в конечном итоге воспользуюсь его решением в будущем. Тем временем я просто собираюсь использовать кабелепровод.

+2

Можете ли вы ссылаться на одного или обоих потребителей в качестве примера? Большинство кодов труб, с которыми я знаком, не использует базовое значение потребителя, не используется таким образом. Если вы не используете базовое значение, с другой стороны, это тривиальный zip двух потребителей вместе. – Davorak

ответ

2

Я думаю, что что-то не так с тем, как вы это делаете, по причинам, которые Даварак упоминает в своем замечании. Но если вам действительно нужна такая функция, вы можете ее определить.

import Pipes.Internal 
import Pipes.Core 

zipConsumers :: Monad m => Consumer a m r -> Consumer a m s -> Consumer a m (r,s) 
zipConsumers p q = go (p,q) where 
    go (p,q) = case (p,q) of 
    (Pure r  , Pure s)  -> Pure (r,s) 
    (M mpr  , ps)   -> M (do pr <- mpr 
             return (go (pr, ps))) 
    (pr   , M mps)  -> M (do ps <- mps 
             return (go (pr, ps))) 
    (Request _ f, Request _ g) -> Request() (\a -> go (f a, g a)) 
    (Request _ f, Pure s)  -> Request() (\a -> do r <- f a 
                 return (r, s)) 
    (Pure r  , Request _ g) -> Request() (\a -> do s <- g a 
                 return (r,s)) 
    (Respond x _, _   ) -> closed x 
    (_   , Respond y _) -> closed y 

Если вы «сжать» потребители, не используя их возвращаемое значение, только их «эффекты» вы можете просто использовать tee consumer1 >-> consumer2

0

Потребитель формирует Монаду так

combineConsumers = liftM2 (,) 

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

1

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

+1

Хорошо, что удобно. Учитывая существование attoparsec-кабелепровода, возможно, я должен рассмотреть возможность использования кабелепровода вместо этого в этом случае. –

3

Результаты «моноидальные», вы можете использовать функцию tee из прелюдии Pipes, в сочетании с WriterT.

{-# LANGUAGE OverloadedStrings #-} 

import Data.Monoid 
import Control.Monad 
import Control.Monad.Writer 
import Control.Monad.Writer.Class 
import Pipes 
import qualified Pipes.Prelude as P 
import qualified Data.Text as T 

textSource :: Producer T.Text IO() 
textSource = yield "foo" >> yield "bar" >> yield "foo" >> yield "nah" 

counter :: Monoid w => T.Text 
        -> (T.Text -> w) 
        -> Consumer T.Text (WriterT w IO)() 
counter word inject = P.filter (==word) >-> P.mapM (tell . inject) >-> P.drain 

main :: IO() 
main = do 
    result <-runWriterT $ runEffect $ 
     hoist lift textSource >-> 
     P.tee (counter "foo" inject1) >-> (counter "bar" inject2) 
    putStrLn . show $ result 
    where 
    inject1 _ = (,) (Sum 1) mempty 
    inject2 _ = (,) mempty (Sum 1) 

Update: Как уже говорилось в комментарии, реальная проблема, которую я вижу, что в pipes анализаторами не Consumers. И как вы можете запускать два парсера одновременно, если они имеют разные формы поведения в отношении остатков? Что произойдет, если один из парсеров хочет «разукрасить» какой-то текст, а другой - нет?

Одним из возможных решений является запуск парсеров по-настоящему одновременным образом в разных потоках. Примитивы в пакете pipes-concurrency позволяют «дублировать» Producer, записывая одни и те же данные в два разных почтовых ящика. И тогда каждый парсер может делать все, что захочет, с собственной копией продюсера.Вот пример, который также использует pipes-parse, pipes-attoparsec и async пакеты:

{-# LANGUAGE OverloadedStrings #-} 

import Data.Monoid 
import qualified Data.Text as T 
import Data.Attoparsec.Text hiding (takeWhile) 
import Data.Attoparsec.Combinator 
import Control.Applicative 
import Control.Monad 
import Control.Monad.State.Strict 
import Pipes 
import qualified Pipes.Prelude as P 
import qualified Pipes.Attoparsec as P 
import qualified Pipes.Concurrent as P 
import qualified Control.Concurrent.Async as A 

parseChars :: Char -> Parser [Char] 
parseChars c = fmap mconcat $ 
    many (notChar c) *> many1 (some (char c) <* many (notChar c)) 

textSource :: Producer T.Text IO() 
textSource = yield "foo" >> yield "bar" >> yield "foo" >> yield "nah" 

parseConc :: Producer T.Text IO() 
      -> Parser a 
      -> Parser b 
      -> IO (Either P.ParsingError a,Either P.ParsingError b) 
parseConc producer parser1 parser2 = do 
    (outbox1,inbox1,seal1) <- P.spawn' P.Unbounded 
    (outbox2,inbox2,seal2) <- P.spawn' P.Unbounded 
    feeding <- A.async $ runEffect $ producer >-> P.tee (P.toOutput outbox1) 
               >->  P.toOutput outbox2 
    sealing <- A.async $ A.wait feeding >> P.atomically seal1 >> P.atomically seal2 
    r <- A.runConcurrently $ 
     (,) <$> (A.Concurrently $ parseInbox parser1 inbox1) 
      <*> (A.Concurrently $ parseInbox parser2 inbox2) 
    A.wait sealing 
    return r 
    where 
    parseInbox parser inbox = evalStateT (P.parse parser) (P.fromInput inbox) 

main :: IO() 
main = do 
    (Right a, Right b) <- parseConc textSource (parseChars 'o') (parseChars 'a') 
    putStrLn . show $ (a,b) 

Результат:

("oooo","aa") 

Я не знаю, как много накладных расходов этот подход представляет.

+0

К сожалению, мои результаты не моноидальны. Наверное, я мог бы использовать состояние. Полагаю, я мог бы также использовать iorefs. –

+0

@mindreader Вы всегда можете использовать 'First' и' Last' из 'Data.Monoid' «сделать моноид» из любого типа. Реальная проблема, которую я вижу, заключается в том, что в 'pipe',' Parsers' не «Потребители». Кроме того, оставшаяся обработка при запуске двух парсеров в paralell, вероятно, будет сложной. – danidiaz

2

идиоматическое решение переписать ваши Consumer S как Fold или FoldM из foldl, а затем объединить их, используя Applicative стиль. Затем вы можете преобразовать эту объединенную сгиб в ту, которая работает на трубах.

Давайте предположим, что вы либо два Fold S:

fold1 :: Fold a r1 
fold2 :: Fold a r2 

... или два FoldM S:

foldM1 :: Monad m => FoldM a m r1 
foldM2 :: Monad m => FoldM a m r2 

Затем объединить их в единый Fold/FoldM используя Applicative стиль:

import Control.Applicative 

foldBoth :: Fold a (r1, r2) 
foldBoth = (,) <$> fold1 <*> fold2 

foldBothM :: Monad m => FoldM a m (r1, r2) 
foldBothM = (,) <$> foldM1 <*> foldM2 

-- or: foldBoth = liftA2 (,) fold1 fold2 
--  foldMBoth = liftA2 (,) foldM1 foldM2 

Вы можете превратить либо сгиб в складную футляр Pipes.Prelude, либо Parser. Вот необходимые функции преобразования:

import Control.Foldl (purely, impurely) 
import qualified Pipes.Prelude as Pipes 
import qualified Pipes.Parse as Parse 

purely Pipes.fold 
    :: Monad m => Fold a b -> Producer a m() -> m b 

impurely Pipes.foldM 
    :: Monad m => FoldM m a b -> Producer a m() -> m b 

purely Parse.foldAll 
    :: Monad m => Fold a b -> Parser a m r 

impurely Parse.foldMAll 
    :: Monad m => FoldM a m b -> Parser a m r 

Причина функций purely и impurely так, что foldl и pipes могут взаимодействовать без либо один подвергаясь зависимость от другого. Кроме того, они позволяют библиотекам, отличным от pipes (например, conduit) повторно использовать foldl (подсказка, @MichaelSnoyman).

Я извиняюсь, что эта функция не документирована, в основном потому, что мне потребовалось некоторое время, чтобы выяснить, как получить pipes и foldl взаимодействовать в форме зависимостей свободной, и это было после того, как я написал pipes учебник. Я обновлю учебник, чтобы указать на этот трюк.

Чтобы узнать, как использовать foldl, просто прочтите the documentation в основном модуле. Это очень маленькая и простая в освоении библиотека.

Смежные вопросы