简体   繁体   English

在haskell中将管道组合成一个循环或循环

[英]Composing Pipes into a loop or cycle in haskell

This question is about the Haskell library Pipes .这个问题是关于 Haskell 库Pipes 的

This question is related to 2019 Advent of Code Day 11 (possible spoiler warning)此问题与 2019 Advent of Code Day 11 (可能剧透警告)有关

I have two Pipe Int Int mr brain and robot that need to pass information too each other in a continuous loop.我有两个Pipe Int Int mr brainrobot ,它们也需要在连续循环中相互传递信息。 That is the output of brain need to go to the input of robot and the output of robot needs to go the the input of brain .这是输出brain需要去的输入robot的输出和robot需要走的输入brain When brain finished I need the result of the computation.brain完成时,我需要计算的结果。

How do I compose brain and robot into a loop?如何将brainrobot成一个循环? Ideally a loop with the type Effect mr that I can pass to runEffect理想情况下,我可以传递给runEffect类型为Effect mr的循环

Edit: The result should look like this:编辑:结果应该是这样的:

   +-----------+     +-----------+   
   |           |     |           |   
   |           |     |           |   
a ==>    f    ==> b ==>    g    ==> a=|
^  |           |     |           |    |
|  |     |     |     |     |     |    |
|  +-----|-----+     +-----|-----+    |
|        v                 v          |
|        ()                r          |
+=====================================+

The answer答案

The easiest solution would be to use Client and Server as danidiaz suggested in the comments, since pipes doesn't have any built in support for cyclic pipes and it would be incredibly difficult, if not impossible to do so correctly.最简单的解决方案是使用 danidiaz 在评论中建议的ClientServer ,因为pipes没有任何对循环管道的内置支持,如果正确地这样做的话,这将是非常困难的。 This is mostly because we need to handle cases where the number of await s doesn't match the number of yield s.这主要是因为我们需要处理await的数量与yield的数量不匹配的情况。

Edit: I added a section about the problems with the other answer.编辑:我添加了一个关于其他答案问题的部分。 See section "Another problematic alternative"请参阅“另一个有问题的替代方案”部分

Edit 2: I have added a less problematic possible solution below.编辑 2:我在下面添加了一个问题较少的可能解决方案。 See the section "A possible solution"请参阅“可能的解决方案”部分

A problematic alternative有问题的替代方案

It is however possible to simulate it with the help of the Proxy framework (with Client and Server ) and the neat function generalize , which turns a unidirectional Pipe into a bidirectional Proxy .然而,可以借助Proxy框架(带有ClientServer )和简洁的函数generalize来模拟它,它将单向Pipe变成双向Proxy

                                       generalize f x0
   +-----------+                   +---------------------+
   |           |                   |                     |
   |           |                x <======================== x
a ==>    f    ==> b   becomes      |                     |
   |           |                a ==>         f         ==> b
   |     |     |                   |                     |
   +-----|-----+                   +----------|----------+
         v                                    v     
         r                                    r     

Now we can use //> and >\\\\ to plug the ends and make the flow cyclic:现在我们可以使用//>>\\\\来插入末端并使流循环:

loop :: Monad m => Pipe a a m r -> a -> Effect m r
loop p x0 = pure >\\ generalize p x0 //> pure

which has this shape有这个形状

            loop f

              a 
        +-----|-----+
        |     |     |
 /====<=======/===<========\
 |      |           |      |
 \=> a ==>    f    ==> a ==/
        |           |
        +-----|-----+
              v    
              r    

As you can see, we are required to input an initial value for a .如您所见,我们需要为a输入初始值。 This is because there is no guarantee that the pipe won't await before it yields, which would force it to wait forever.这是因为无法保证管道在产生之前不会await ,这将迫使它永远等待。

Note however that this will throw away data if the pipe yield s multiple times before await ing, since generalize is internally implemented with a state monad that saves the last value when yielding and retrieves the last value when awaiting.但是请注意,如果管道在await之前多次yield s,这将丢弃数据,因为 generalize 在内部使用状态 monad 实现,该状态 monad 在yield时保存最后一个值并在等待时检索最后一个值。

Usage (of the problematic idea)用法(有问题的想法)

To use it with your pipes, simply compose them and give them to loop :要将它与您的管道一起使用,只需将它们组合起来并让它们loop

runEffect $ loop (f >-> g)

But please don't use it, since it will randomly throw away data if you are not careful但是请不要使用它,因为如果您不小心它会随机丢弃数据

Another problematic alternative另一个有问题的选择

You could also make a lazily infinite chain of pipes like mingmingrr suggested你也可以像mingmingrr建议的那样制作一个懒惰的无限管道链

infiniteChain :: Functor m => Pipe a a m r -> Producer a m r
infiniteChain f = infiniteChain >-> f

This solves the problem of discarded/duplicated values, but has several other problems.这解决了丢弃/重复值的问题,但还有其他几个问题。 First is that awaiting first before yielding will cause an infinite loop with infinite memory usage, but that is already addressed in mingmingrr's answer.首先,在让步之前先等待会导致无限循环使用无限内存,但这已经在 mingmingrr 的回答中解决了。

Another, more difficult to solve, issue is that every action before the corresponding yield is duplicated once for each await.另一个更难解决的问题是,在相应的 yield 之前的每个 action 都会为每个 await 重复一次。 We can see this if we modify their example to log what is happening:如果我们修改他们的示例以记录正在发生的事情,我们可以看到这一点:

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

Now, running runEffect (cyclic' 0 >-> P.print) will print the following:现在,运行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

As you can see, for each await , we re-executed everything until the corresponding yield .如您所见,对于每个await ,我们重新执行所有内容,直到相应的yield More specifically, an await triggers a new copy of the pipe to run until it reaches a yield.更具体地说,等待触发管道的新副本运行,直到达到产量。 When we await again, the copy will run until the next yield again and if it triggers an await during that, it will create yet another copy and run it until the first yield, and so on.当我们再次等待时,该副本将再次运行直到下一次产生,如果在此期间触发await ,它将创建另一个副本并运行它直到第一次产生,依此类推。

This means that in the best case, we get O(n^2) instead of linear performance (And using O(n) instead of O(1) memory), since we are repeating everything for each action.这意味着在最好的情况下,我们得到O(n^2)而不是线性性能(并且使用O(n)而不是O(1)内存),因为我们为每个动作重复所有内容。 In the worst case, eg if we were reading from or writing to a file, we could get completely wrong results since we are repeating side-effects.在最坏的情况下,例如,如果我们正在读取或写入文件,我们可能会得到完全错误的结果,因为我们正在重复副作用。

A possible solution一个可能的解决方案

If you really must use Pipe s and can't use request / respond instead and you are sure that your code will never await more than (or before) it yield s (or have a good default to give it in those cases), we could build upon my previous attempt above to make a solution that at least handles the case when yield ing more than you await .如果您真的必须使用Pipe并且不能使用request / respond代替,并且您确定您的代码永远不会await超过(或之前)它yield s(或者在这些情况下有一个很好的默认值),我们可以在我之前的尝试的基础上提出一个解决方案,该解决方案至少可以在yield多于await时处理这种情况。

The trick is adding a buffer to the implementation of generalize , so the excess values are stored instead of being thrown away.诀窍是在generalize的实现中添加一个缓冲区,这样多余的值就会被存储而不是被丢弃。 We can also keep the extra argument as a default value for when the buffer is empty.我们还可以将额外参数保留为缓冲区为空时的默认值。

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)

If we now plug this into our definition of of loop , we will have solved the problem of discarding excess values (at the memory cost of keeping a buffer).如果我们现在将其插入loop的定义中,我们将解决丢弃多余值的问题(以保留缓冲区的内存成本为代价)。 It also prevents duplicating any values other than the default value and only uses the default value when the buffer is empty.它还可以防止复制除默认值之外的任何值,并且仅在缓冲区为空时使用默认值。

loop' :: Monad m => a -> Pipe a a m r -> Effect m r
loop' x0 p = pure >\\ generalize' p x0 //> pure

If we want await ing before yield ing to be an error, we can simply give error as our default value: loop' (error "Await without yield") somePipe .如果我们希望在yield之前await是一个错误,我们可以简单地将error作为我们的默认值: loop' (error "Await without yield") somePipe

TL;DR TL; 博士

Use Client and Server from Pipes.Core .使用Pipes.Core ClientServer It will solve your problem and not cause a ton of strange bugs.它将解决您的问题,而不会导致大量奇怪的错误。

If that is not possible, my "Possible solution" section with a modified version of generalize should do the job in most cases.如果这是不可能的,在大多数情况下,我的“可能的解决方案”部分带有generalize的修改版本应该可以完成这项工作。

You can make a cyclic pipe by tying the output of the pipe into the input.您可以通过将管道的输出连接到输入来制作循环管道。

cyclic :: Functor m => Producer a m r
cyclic = cyclic >-> f >-> g

Consider the following example:考虑以下示例:

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)

Since neither f nor g here yields any output before awaiting, using cyclic = cyclic >-> f >-> g would result in f awaiting forever.由于这里的fg在等待之前都不会产生任何输出,因此使用cyclic = cyclic >-> f >-> g将导致f永远等待。 The key to avoiding this is making sure either f or g yields something before awaiting, or feeding in the initial input to the first pipe like so:避免这种情况的关键是确保fg在等待之前产生一些东西,或者像这样将初始输入输入到第一个管道:

cyclic' :: Monad m => Int -> Producer Int m Int
cyclic' input = let pipe = (yield input >> pipe) >-> f >-> g 6 in pipe

Here running runEffect (cyclic' 0 >-> P.print) gives return 100 and prints 1 3 7 15 31 63 .这里运行runEffect (cyclic' 0 >-> P.print) return 100并打印1 3 7 15 31 63

PS (possible Advent of Code 2019 spoilers) You can use this same scheme to complete day 7. If your Intcode computer has type StateT IntcodeState (Pipe Int Int m) , then you can use replicate 5 (evalState runIntcode initialIntcodeState) to get 5 pipes corresponding to each of the 5 amplifiers. 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM