[英]Composing Pipes into a loop or cycle in haskell
這個問題是關於 Haskell 庫Pipes 的。
此問題與 2019 Advent of Code Day 11 (可能劇透警告)有關
我有兩個Pipe Int Int mr
brain
和robot
,它們也需要在連續循環中相互傳遞信息。 這是輸出brain
需要去的輸入robot
的輸出和robot
需要走的輸入brain
。 當brain
完成時,我需要計算的結果。
如何將brain
和robot
成一個循環? 理想情況下,我可以傳遞給runEffect
類型為Effect mr
的循環
編輯:結果應該是這樣的:
+-----------+ +-----------+
| | | |
| | | |
a ==> f ==> b ==> g ==> a=|
^ | | | | |
| | | | | | | |
| +-----|-----+ +-----|-----+ |
| v v |
| () r |
+=====================================+
最簡單的解決方案是使用 danidiaz 在評論中建議的Client
和Server
,因為pipes
沒有任何對循環管道的內置支持,如果正確地這樣做的話,這將是非常困難的。 這主要是因為我們需要處理await
的數量與yield
的數量不匹配的情況。
編輯:我添加了一個關於其他答案問題的部分。 請參閱“另一個有問題的替代方案”部分
編輯 2:我在下面添加了一個問題較少的可能解決方案。 請參閱“可能的解決方案”部分
然而,可以借助Proxy
框架(帶有Client
和Server
)和簡潔的函數generalize
來模擬它,它將單向Pipe
變成雙向Proxy
。
generalize f x0
+-----------+ +---------------------+
| | | |
| | x <======================== x
a ==> f ==> b becomes | |
| | a ==> f ==> b
| | | | |
+-----|-----+ +----------|----------+
v v
r r
loop :: Monad m => Pipe a a m r -> a -> Effect m r
loop p x0 = pure >\\ generalize p x0 //> pure
有這個形狀
loop f
a
+-----|-----+
| | |
/====<=======/===<========\
| | | |
\=> a ==> f ==> a ==/
| |
+-----|-----+
v
r
如您所見,我們需要為a
輸入初始值。 這是因為無法保證管道在產生之前不會await
,這將迫使它永遠等待。
但是請注意,如果管道在await
之前多次yield
s,這將丟棄數據,因為 generalize 在內部使用狀態 monad 實現,該狀態 monad 在yield
時保存最后一個值並在等待時檢索最后一個值。
要將它與您的管道一起使用,只需將它們組合起來並讓它們loop
:
runEffect $ loop (f >-> g)
但是請不要使用它,因為如果您不小心它會隨機丟棄數據
你也可以像mingmingrr建議的那樣制作一個懶惰的無限管道鏈
infiniteChain :: Functor m => Pipe a a m r -> Producer a m r
infiniteChain f = infiniteChain >-> f
這解決了丟棄/重復值的問題,但還有其他幾個問題。 首先,在讓步之前先等待會導致無限循環使用無限內存,但這已經在 mingmingrr 的回答中解決了。
另一個更難解決的問題是,在相應的 yield 之前的每個 action 都會為每個 await 重復一次。 如果我們修改他們的示例以記錄正在發生的事情,我們可以看到這一點:
import Pipes
import qualified Pipes.Prelude as P
f :: Monad m => Pipe Int Int m r
f = P.map (* 2)
g :: Monad m => Int -> Pipe Int Int m ()
g 0 = return ()
g n = do
lift . putStrLn $ "Awaiting. n = " ++ show n
x <- await
lift . putStrLn $ "Got: x = " ++ show x ++ " and n = "++ show n ;
yield (x + 1)
g (n - 1)
cyclic' :: Monad m => Int -> Producer Int m Int
cyclic' input = let pipe = (yield input >> pipe) >-> f >-> g 6 in pipe
現在,運行runEffect (cyclic' 0 >-> P.print)
將打印以下內容:
Awaiting. n = 6
Got: x = 0 and n = 6
1
Awaiting. n = 5
Awaiting. n = 6
Got: x = 0 and n = 6
Got: x = 2 and n = 5
3
Awaiting. n = 4
Awaiting. n = 5
Awaiting. n = 6
Got: x = 0 and n = 6
Got: x = 2 and n = 5
Got: x = 6 and n = 4
7
Awaiting. n = 3
Awaiting. n = 4
Awaiting. n = 5
Awaiting. n = 6
Got: x = 0 and n = 6
Got: x = 2 and n = 5
Got: x = 6 and n = 4
Got: x = 14 and n = 3
15
Awaiting. n = 2
Awaiting. n = 3
Awaiting. n = 4
Awaiting. n = 5
Awaiting. n = 6
Got: x = 0 and n = 6
Got: x = 2 and n = 5
Got: x = 6 and n = 4
Got: x = 14 and n = 3
Got: x = 30 and n = 2
31
Awaiting. n = 1
Awaiting. n = 2
Awaiting. n = 3
Awaiting. n = 4
Awaiting. n = 5
Awaiting. n = 6
Got: x = 0 and n = 6
Got: x = 2 and n = 5
Got: x = 6 and n = 4
Got: x = 14 and n = 3
Got: x = 30 and n = 2
Got: x = 62 and n = 1
63
如您所見,對於每個await
,我們重新執行所有內容,直到相應的yield
。 更具體地說,等待觸發管道的新副本運行,直到達到產量。 當我們再次等待時,該副本將再次運行直到下一次產生,如果在此期間觸發await
,它將創建另一個副本並運行它直到第一次產生,依此類推。
這意味着在最好的情況下,我們得到O(n^2)
而不是線性性能(並且使用O(n)
而不是O(1)
內存),因為我們為每個動作重復所有內容。 在最壞的情況下,例如,如果我們正在讀取或寫入文件,我們可能會得到完全錯誤的結果,因為我們正在重復副作用。
如果您真的必須使用Pipe
並且不能使用request
/ respond
代替,並且您確定您的代碼永遠不會await
超過(或之前)它yield
s(或者在這些情況下有一個很好的默認值),我們可以在我之前的嘗試的基礎上提出一個解決方案,該解決方案至少可以在yield
多於await
時處理這種情況。
訣竅是在generalize
的實現中添加一個緩沖區,這樣多余的值就會被存儲而不是被丟棄。 我們還可以將額外參數保留為緩沖區為空時的默認值。
import Pipes.Lift (evalStateP)
import Control.Monad.Trans.State.Strict (state, modify)
import qualified Data.Sequence
generalize' :: Monad m => Pipe a b m r -> x -> Proxy x a x b m r
generalize' p x0 = evalStateP Seq.empty $ up >\\ hoist lift p //> dn
where
up () = do
x <- lift $ state (takeHeadDef x0)
request x
dn a = do
x <- respond a
lift $ modify (Seq.|> x)
takeHeadDef :: a -> Seq.Seq a -> (a, Seq.Seq a)
takeHeadDef x0 xs = (foldr const x0 xs, Seq.drop 1 xs)
如果我們現在將其插入loop
的定義中,我們將解決丟棄多余值的問題(以保留緩沖區的內存成本為代價)。 它還可以防止復制除默認值之外的任何值,並且僅在緩沖區為空時使用默認值。
loop' :: Monad m => a -> Pipe a a m r -> Effect m r
loop' x0 p = pure >\\ generalize' p x0 //> pure
如果我們希望在yield
之前await
是一個錯誤,我們可以簡單地將error
作為我們的默認值: loop' (error "Await without yield") somePipe
。
使用Pipes.Core
Client
和Server
。 它將解決您的問題,而不會導致大量奇怪的錯誤。
如果這是不可能的,在大多數情況下,我的“可能的解決方案”部分帶有generalize
的修改版本應該可以完成這項工作。
您可以通過將管道的輸出連接到輸入來制作循環管道。
cyclic :: Functor m => Producer a m r
cyclic = cyclic >-> f >-> g
考慮以下示例:
import Pipes
import qualified Pipes.Prelude as P
f :: Monad m => Pipe Int Int m r
f = P.map (* 2)
g :: Monad m => Int -> Pipe Int Int m Int
g 0 = return 100
g n = do x <- await ; yield (x + 1) ; g (n - 1)
由於這里的f
和g
在等待之前都不會產生任何輸出,因此使用cyclic = cyclic >-> f >-> g
將導致f
永遠等待。 避免這種情況的關鍵是確保f
或g
在等待之前產生一些東西,或者像這樣將初始輸入輸入到第一個管道:
cyclic' :: Monad m => Int -> Producer Int m Int
cyclic' input = let pipe = (yield input >> pipe) >-> f >-> g 6 in pipe
這里運行runEffect (cyclic' 0 >-> P.print)
return 100
並打印1 3 7 15 31 63
。
PS(可能出現 Code 2019 劇透)您可以使用相同的方案來完成第 7 天。如果您的 Intcode 計算機類型為StateT IntcodeState (Pipe Int Int m)
,那么您可以使用replicate 5 (evalState runIntcode initialIntcodeState)
來獲得 5 個管道對應於 5 個放大器中的每一個。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.