This question is about the Haskell library Pipes.
This question is related to 2019 Advent of Code Day 11 (possible spoiler warning)
I have two Pipe Int Int m r brain and robot that need to pass information too each other in a continuous loop. 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. When brain finished I need the result of the computation.
How do I compose brain and robot into a loop? Ideally a loop with the type Effect m r that I can pass to runEffect
Edit: The result should look like this:
+-----------+ +-----------+
| | | |
| | | |
a ==> f ==> b ==> g ==> a=|
^ | | | | |
| | | | | | | |
| +-----|-----+ +-----|-----+ |
| v v |
| () r |
+=====================================+
The answer
The easiest solution would be to use
ClientandServeras danidiaz suggested in the comments, sincepipesdoesn't have any built in support for cyclic pipes and it would be incredibly difficult, if not impossible to do so correctly. This is mostly because we need to handle cases where the number ofawaits doesn't match the number ofyields.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. See the section "A possible solution"
A problematic alternative
It is however possible to simulate it with the help of the
Proxyframework (withClientandServer) and the neat functiongeneralize, which turns a unidirectionalPipeinto a bidirectionalProxy.Now we can use
//>and>\\to plug the ends and make the flow cyclic:which has this shape
As you can see, we are required to input an initial value for
a. This is because there is no guarantee that the pipe won'tawaitbefore it yields, which would force it to wait forever.Note however that this will throw away data if the pipe
yields multiple times beforeawaiting, since generalize is internally implemented with a state monad that saves the last value when yielding and retrieves the last value when awaiting.Usage (of the problematic idea)
To use it with your pipes, simply compose them and give them to
loop: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
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.
Another, more difficult to solve, issue is that every action before the corresponding yield is duplicated once for each await. We can see this if we modify their example to log what is happening:
Now, running
runEffect (cyclic' 0 >-> P.print)will print the following:As you can see, for each
await, we re-executed everything until the correspondingyield. 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 anawaitduring that, it will create yet another copy and run it until the first yield, and so on.This means that in the best case, we get
O(n^2)instead of linear performance (And usingO(n)instead ofO(1)memory), since we are repeating everything for each action. In the worst case, e.g. 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
Pipes and can't userequest/respondinstead and you are sure that your code will neverawaitmore than (or before) ityields (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 whenyielding more than youawait.The trick is adding a buffer to the implementation of
generalize, so the excess values are stored instead of being thrown away. We can also keep the extra argument as a default value for when the buffer is empty.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). It also prevents duplicating any values other than the default value and only uses the default value when the buffer is empty.If we want
awaiting beforeyielding to be an error, we can simply giveerroras our default value:loop' (error "Await without yield") somePipe.TL;DR
Use
ClientandServerfromPipes.Core. 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
generalizeshould do the job in most cases.