This question is about the Haskell Pipes library
Background:
In a previous question, I asked how to form a cycle using pipes and the answer I got was "don't do that. Use request and response instead." While there is an excellent and clearly written tutorial that covers Producers, Consumers, Pipes, and Effects in plain English. The documentation for request and response Client and Server starts by defining Categories and mentioning some other CompSci concepts like "the generator design pattern." and "the iteratee design pattern." which are never explained. So I'm stuck not knowing how to "use request and response instead."
The Setup
I have two state-machines like thing that need to pass data back and forth repeatedly, robot and intCode.
The Robot is pretty simple:
robot :: Pipe Int Int m r -- robot never returns so its return type is polymorphic
robot = go newRobot
where
go r = do
yield $ color r
c <- toColor <$> await
turn <- toTurn <$> await
go $ update c turn r
It yields a value, awaits two instructions (a new color and a turn), updates the state (r) of the robot, and starts over.
The intCode virtual machine runs programmed to communicate with the robot. It takes a program (called code) and creates a pipe that will await the sensor reading from the robot then yield two instructions to it.
(boot code) :: Pipe Int Int m ()
Let's assume that the IntCode VM is not easily modified, but that the robot is.
Questions:
How are request and respond different from await and yield?
How do I use them to facilitate continuous communication between the robot and the VM?
The definitions of
awaitandyieldare:so they are closely related to
requestandrespond. Theawaitandyieldversions have just been specialized to unidirectional pull-based streams (Producers,Pipes andConsumers).To perform bidirectional communication between two endpoints, you want to set up a
Clientand aServerand connect them.A
Clientis a monadic action that makes requests:by sending request
xand receiving responsey. AServeris a monadic action that responds:by accepting request
xand sending responsey. Note that these operations are symmetric, so in a given application it's arbitrary which half is theClientand which half is theServer.Now, you may notice that while the
Clientsends anxand receives ayin response, theServerseems backward. It sends responseybefore receiving requestx! In fact, it just needs to operate one step behind -- a server in a pull-based stream will want to send its responseyto the previous request in order to receive the next requestx.As a simple example, here's a
Clientthat requests addition of numbers to calculate powers of two:Writing the server to add numbers is a little trickier because of this "one step behind" business. We might start by writing:
The trick is to get things started by accepting the first request as an argument to the monadic action:
Fortunately, the pull point-ful connector
+>>has the right type to connect these:and we can run the resulting effect in the usual manner:
Note that, for this type of bidirectional communication, requests and responses need to run in synchronous lock-step, so you can't do the equivalent of yielding once and awaiting twice. If you wanted to re-design the example above to send requests in two parts, you'd need to develop a protocol with sensible request and response types, like:
For your brain/robot application, you can design the robot as either a client:
or a server:
Because the robot produces output before consuming input, as a client it will fit into a pull-based stream with a brain server:
or as a server it will fit into a push-based stream with a brain client: