有两种不使用并发的方法可以做到这一点,但都有注意事项。
第一种方法是如果pipe1
and pipe2
只是简单Consumer
永远循环如下:
p1 = for cat f -- i.e. p1 = forever $ await >>= f
p2 = for cat g -- i.e. p2 = forever $ await >>= g
...那么解决这个问题的简单方法就是编写:
for P.stdinLn $ \str -> do
f str
g str
例如,如果p1
只是print
计算每个值:
p1 = for cat (lift . print)
... and p2
只是将该值写入句柄:
p2 = for cat (lift . hPutStrLn h)
...然后你可以像这样组合它们:
for P.stdinLn $ \str -> do
lift $ print str
lift $ hPutStrLn h str
然而,这种简化仅适用于Consumer
这是一个简单的循环。还有另一种更通用的解决方案,即定义一个ArrowChoice
管道的实例。我相信基于拉动Pipe
s 不允许正确的守法实例,而是基于推送Pipe
s do:
newtype Edge m r a b = Edge { unEdge :: a -> Pipe a b m r }
instance (Monad m) => Category (Edge m r) where
id = Edge push
(Edge p2) . (Edge p1) = Edge (p1 >~> p2)
instance (Monad m) => Arrow (Edge m r) where
arr f = Edge (push />/ respond . f)
first (Edge p) = Edge $ \(b, d) ->
evalStateP d $ (up \>\ unsafeHoist lift . p />/ dn) b
where
up () = do
(b, d) <- request ()
lift $ put d
return b
dn c = do
d <- lift get
respond (c, d)
instance (Monad m) => ArrowChoice (Edge m r) where
left (Edge k) = Edge (bef >=> (up \>\ (k />/ dn)))
where
bef x = case x of
Left b -> return b
Right d -> do
_ <- respond (Right d)
x2 <- request ()
bef x2
up () = do
x <- request ()
bef x
dn c = respond (Left c)
这需要一个新类型,以便类型参数的顺序为ArrowChoice
期望。
如果您不熟悉术语“基于推送”Pipe
,它基本上是一个Pipe
从最上游的管道开始,而不是从最下游的管道开始,它们都具有以下形状:
a -> Pipe a b m r
把它想象成一个Pipe
在从上游接收到至少一个值之前,它无法“运行”。
这些基于推送的Pipe
s 是传统拉式的“双重”Pipe
s,具有自己的组合运算符和身份:
(>~>) :: (Monad m)
=> (a -> Pipe a b m r)
-> (b -> Pipe b c m r)
-> (a -> Pipe a c m r)
push :: (Monad m)
-> a -> Pipe a a m r
...但是单向Pipes
默认情况下,API 不会导出此内容。您只能从以下位置获取这些运算符Pipes.Core
(您可能想更仔细地研究该模块,以建立对它们如何工作的直觉)。该模块显示两者都基于推送Pipe
s 和基于拉动的Pipe
s 都是更通用的双向版本的特殊情况,了解双向情况就是您了解为什么它们彼此对偶的方式。
一旦你有一个Arrow
对于基于推送的管道的实例,您可以编写如下内容:
p >>> bifurcate >>> (p1 +++ p2)
where
bifurcate = Edge $ pull ~> \a -> do
yield (Left a) -- First give `p1` the value
yield (Right a) -- Then give `p2` the value
那么你会使用runEdge
完成后将其转换为基于拉动的管道。
这种方法有一个主要缺点,即您无法自动将基于拉的管道升级为基于推的管道(但通常不难弄清楚如何手动执行此操作)。例如,要升级Pipes.Prelude.map
是一个基于推送的Pipe
,你会写:
mapPush :: (Monad m) => (a -> b) -> (a -> Pipe a b m r)
mapPush f a = do
yield (f a)
Pipes.Prelude.map f
那么它就有正确的类型被包裹在Arrow
:
mapEdge :: (Monad m) => (a -> b) -> Edge m r a b
mapEdge f = Edge (mapPush f)
当然,更简单的方法是从头开始编写:
mapEdge f = Edge $ push ~> yield . f
使用最适合您的方法。
事实上,我想出了Arrow
and ArrowChoice
正是因为我试图回答与您完全相同的问题:如何在不使用并发的情况下解决此类问题?我在另一个 Stack Overflow 答案中写了一个关于这个更普遍的主题的长答案here,我在其中描述了如何使用这些Arrow
and ArrowChoice
实例将并发系统提炼为等效的纯系统。