将导管与多个输入融合

时间:2013-03-24 02:37:16

标签: haskell conduit

我正在尝试创建一个可以使用多个输入流的管道。我需要能够在没有特定顺序(例如,不交替)的情况下等待输入流中的一个或另一个使得zip无用。这里没有任何平行或不确定的东西:我等待一个流或另一个流。我希望能够编写类似于以下内容的代码(其中awaitAawaitB分别等待第一个或第二个输入流):

do
  _ <- awaitA
  x <- awaitA
  y <- awaitB
  yield (x,y)
  _ <- awaitB
  _ <- awaitB
  y' <- awaitB
  yield (x,y')

我拥有的最佳解决方案是使内部monad成为另一个管道,例如

foo :: Sink i1 (ConduitM i2 o m) ()

然后允许

awaitA = await
awaitB = lift await

这主要是有效的。不幸的是,这似乎使得在外导管完全连接之前很难熔化到内导管。我尝试的第一件事是:

fuseInner :: Monad m =>
                Conduit i2' m i2 -> 
                Sink i1 (ConduitM i2 o m) () -> 
                Sink i1 (ConduitM i2' o m) ()
fuseInner x = transPipe (x =$=)

但这不起作用,至少当x有状态,因为(x =$=)多次运行,每次都有效地重新启动x

有没有办法编写fuseInner,而不是闯入导管的内部(看起来它会非常混乱)?有没有更好的方法来处理多个输入流?我是否只是远远超出了导管设计的范围?

谢谢!

2 个答案:

答案 0 :(得分:3)

如果你想组合两个IO - 生成的流,那么Gabriel的评论是解决方案。

否则,您不能等待两个流,哪个流首先生成一个值。管道是单线程和确定性的 - 它一次只处理一个管道。但是你可以创建一个交错两个流的函数,让他们决定何时切换:

{-# OPTIONS_GHC -fwarn-incomplete-patterns #-}
import Control.Monad (liftM)
import Data.Conduit.Internal (
    Pipe (..), Source, Sink,
    injectLeftovers, ConduitM (..),
    mapOutput, mapOutputMaybe
  )

-- | Alternate two given sources, running one until it yields `Nothing`,
-- then switching to the other one.
merge :: Monad m
      => Source m (Maybe a)
      -> Source m (Maybe b)
      -> Source m (Either a b)
merge (ConduitM l) (ConduitM r) = ConduitM $ goL l r
  where
    goL :: Monad m => Pipe () () (Maybe a) () m () 
                   -> Pipe () () (Maybe b) () m ()
                   -> Pipe () () (Either a b) () m ()
    goL (Leftover l ()) r           = goL l r
    goL (NeedInput _ c) r           = goL (c ()) r
    goL (PipeM mx) r                = PipeM $ liftM (`goL` r) mx
    goL (Done _) r                  = mapOutputMaybe (liftM Right) r
    goL (HaveOutput c f (Just o)) r = HaveOutput (goL c r) f (Left o)
    goL (HaveOutput c f Nothing) r  = goR c r
    -- This is just a mirror copy of goL. We should combine them together to
    -- avoid code repetition.
    goR :: Monad m => Pipe () () (Maybe a) () m ()
                   -> Pipe () () (Maybe b) () m ()
                   -> Pipe () () (Either a b) () m ()
    goR l (Leftover r ())           = goR l r
    goR l (NeedInput _ c)           = goR l (c ())
    goR l (PipeM mx)                = PipeM $ liftM (goR l) mx
    goR l (Done _)                  = mapOutputMaybe (liftM Left) l
    goR l (HaveOutput c f (Just o)) = HaveOutput (goR l c) f (Right o)
    goR l (HaveOutput c f Nothing)  = goL l c

它处理一个源,直到它返回Nothing,然后切换到另一个,等等。如果一个源完成,另一个源被处理到最后。

例如,我们可以组合并交错两个列表:

import Control.Monad.Trans
import Data.Conduit (($$), awaitForever)
import Data.Conduit.List (sourceList)

main =  (merge (sourceList $ concatMap (\x -> [Just x, Just x, Nothing]) [  1..10])
               (sourceList $ concatMap (\x -> [Just x, Nothing]) [101..110]) )
         $$ awaitForever (\x -> lift $ print x)

如果您需要多个来源,merge可以适应类似

的内容
mergeList :: Monad m => [Source m (Maybe a)] -> Source m a

将遍历给定的源列表,直到所有源完成。

答案 1 :(得分:3)

这个可以通过潜入导管的内部来完成。我想避免这种情况,因为它看起来非常凌乱。根据这里的回答,听起来没有办法解决它(但我真的很感激一个更清洁的解决方案)。

关键难点在于(x =$=)是一个纯函数,但要使transPipe给出正确的答案,它需要一种有状态的,类似函数的东西:

data StatefulMorph m n = StatefulMorph
    { stepStatefulMorph :: forall a. m a -> n (StatefulMorph m n, a)
    , finalizeStatefulMorph :: n () }

单步StatefulMorph m n会在m中获取一个值,并在n中返回该值和下一个StatefulMorph,这些值应该用于转换下一个m StatefulMorph 1}}价值。最后(x =$=)应该最终确定(在“有状态x”的情况下,最终确定StatefulMorph管道。

管道融合可以pipeL实施,使用fuseStateful :: Monad m => Conduit a m b -> StatefulMorph (ConduitM b c m) (ConduitM a c m) 的代码进行微小更改。签名是:

transPipe

我还需要替换使用hoist值而不是函数的StatefulMorphclass StatefulHoist t where statefulHoist :: (Monad m, Monad n) => StatefulMorph m n -> t m r -> t n r 的特殊情况)。

StatefulHoist

可以使用ConduitM i o的代码编写transPipe fuseInner个实例,但稍作修改。

fuseInner :: Monad m => Conduit a m b -> ConduitM i o (ConduitM b c m) r -> ConduitM i o (ConduitM a c m) r fuseInner left = statefulHoist (fuseStateful left) 易于实施。

{{1}}

我已经写了更详细的解释here并发布了完整的代码here。如果有人可以提出更清洁的解决方案,或使用管道公共API的解决方案,请发布。

感谢所有的建议和意见!