我正在尝试创建一个可以使用多个输入流的管道。我需要能够在没有特定顺序(例如,不交替)的情况下等待输入流中的一个或另一个使得zip无用。这里没有任何平行或不确定的东西:我等待一个流或另一个流。我希望能够编写类似于以下内容的代码(其中awaitA
和awaitB
分别等待第一个或第二个输入流):
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,而不是闯入导管的内部(看起来它会非常混乱)?有没有更好的方法来处理多个输入流?我是否只是远远超出了导管设计的范围?
谢谢!
答案 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
值而不是函数的StatefulMorph
(class 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的解决方案,请发布。
感谢所有的建议和意见!