簡體   English   中英

Haskell:在不使用spawn的情況下拆分管道(廣播)

[英]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實例。 有這么好的理由嗎? 第一種方法真的是拆分管道最干凈的方法嗎?

有兩種方法可以在不使用並發的情況下執行此操作,兩者都有警告。

第一種方法是,如果pipe1pipe2只是簡單的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

使用最適合您的方法。

事實上,我想出了ArrowArrowChoice實例,正是因為我試圖回答與你完全相同的問題:如何在不使用並發的情況下解決這些問題? 我在這里的另一個Stack Overflow答案中寫了一篇關於這個更一般主題的長答案,在這里我描述了如何使用這些ArrowArrowChoice實例將並發系統提煉成等效的純系統。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM