[英]Haskell: Splitting pipes (broadcast) without using spawn
這個問題有點像高爾夫球和很多新手。
我在Haskell中使用了很棒的pipes
庫,我想拆分管道以沿多個通道發送相同的數據(進行廣播)。 Pipes.Concurrent
教程建議使用spawn
來創建郵箱,利用Output
的monoid狀態。 例如,我們可能會這樣做:
main = do
(output1, input1) <- spawn Unbounded
(output2, input2) <- spawn Unbounded
let effect1 = fromInput input1 >-> pipe1
let effect2 = fromInput input2 >-> pipe2
let effect3 = P.stdinLn >-> toOutput (output1 <> output2)
...
通過郵箱的這種間接是否真的有必要? 我們可以改寫這樣的東西嗎?
main = do
let effect3 = P.stdinLn >-> (pipe1 <> pipe2)
...
上面沒有編譯,因為Pipe
沒有Monoid
實例。 有這么好的理由嗎? 第一種方法真的是拆分管道最干凈的方法嗎?
有兩種方法可以在不使用並發的情況下執行此操作,兩者都有警告。
第一種方法是,如果pipe1
和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)
...而p2
只是將該值寫入句柄:
p2 = for cat (lift . hPutStrLn h)
...然后你會像這樣組合它們:
for P.stdinLn $ \str -> do
lift $ print str
lift $ hPutStrLn h str
但是,這種簡化只適用於那些簡單循環的Consumer
。 還有另一個更通用的解決方案,即為管道定義ArrowChoice
實例。 我認為基於拉的Pipe
不允許正確的守法實例,但基於推進的Pipe
做:
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)
這需要一個newtype,以便類型參數按ArrowChoice
期望的順序排列。
如果你不熟悉與基於長期推Pipe
,它基本上是一個Pipe
,從最上游的管道,而不是最下游管開始,他們都具有以下形態:
a -> Pipe a b m r
將它想象為一個Pipe
,在它從上游收到至少一個值之前不能“走”。
這些基於推進的Pipe
是傳統拉式Pipe
的“雙重”,具有自己的組合操作符和標識:
(>~>) :: (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
和基於拉的Pipe
都是更一般的雙向版本的特殊情況,並且理解雙向情況是您如何了解它們彼此的對偶。
為基於推送的管道創建一個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
和ArrowChoice
實例,正是因為我試圖回答與你完全相同的問題:如何在不使用並發的情況下解決這些問題? 我在這里的另一個Stack Overflow答案中寫了一篇關於這個更一般主題的長答案,在這里我描述了如何使用這些Arrow
和ArrowChoice
實例將並發系統提煉成等效的純系統。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.