问题
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 yield
s a value, await
s 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?
回答1:
The definitions of await
and yield
are:
await = request ()
yield = respond
so they are closely related to request
and respond
. The await
and yield
versions have just been specialized to unidirectional pull-based streams (Producer
s, Pipe
s and Consumer
s).
To perform bidirectional communication between two endpoints, you want to set up a Client
and a Server
and connect them.
A Client
is a monadic action that makes requests:
y <- request x
by sending request x
and receiving response y
. A Server
is a monadic action that responds:
x <- respond y
by accepting request x
and sending response y
. Note that these operations are symmetric, so in a given application it's arbitrary which half is the Client
and which half is the Server
.
Now, you may notice that while the Client
sends an x
and receives a y
in response, the Server
seems backward. It sends response y
before receiving request x
! In fact, it just needs to operate one step behind -- a server in a pull-based stream will want to send its response y
to the previous request in order to receive the next request x
.
As a simple example, here's a Client
that requests addition of numbers to calculate powers of two:
-- |Client to generate powers of two
power2 :: Client (Int, Int) Int IO ()
power2 = go 1
where go n | n <= 1024 = do
liftIO $ print n
n' <- request (n,n) -- ask adder to add "n" and "n"
go n'
go n = liftIO $ print "Done"
Writing the server to add numbers is a little trickier because of this "one step behind" business. We might start by writing:
-- |Server to sum numbers
sum2 :: Server (Int, Int) Int IO ()
sum2 = do
(n,n) <- respond ??? -- send previous response to get current request
let n' = n+n
??? <- respond n' -- send current reponse to get next request
The trick is to get things started by accepting the first request as an argument to the monadic action:
-- |Server to sum numbers
sum2 :: (Int, Int) -> Server (Int, Int) Int IO ()
sum2 (m, n) = do
(m', n') <- respond (m+n) -- send response to get next request
sum2 (m', n') -- and loop
Fortunately, the pull point-ful connector +>>
has the right type to connect these:
mypipe :: Effect IO ()
mypipe = sum2 +>> power2
and we can run the resulting effect in the usual manner:
main :: IO ()
main = runEffect mypipe
ghci> main
1
2
4
8
16
32
64
128
256
512
1024
"Done"
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:
data Req = First Int | Second Int
data Res = AckFirst | Answer Int
power2 = ...
AckFirst <- request n
Answer n' <- request n
sum2 = ...
First m' <- respond (Answer (m+n))
Second n' <- respond AckFirst
...
For your brain/robot application, you can design the robot as either a client:
robotC :: Client Color (Color,Turn) Identity ()
robotC = go newRobot
where
go r = do
(c, turn) <- request (color r)
go $ update c turn r
or a server:
robotS :: Server (Color,Turn) Color Identity ()
robotS = go newRobot
where
go r = do
(c, turn) <- respond (color r)
go $ update c turn r
Because the robot produces output before consuming input, as a client it will fit into a pull-based stream with a brain server:
brainS :: Color -> Server Color (Color,Turn) Identity ()
brainS = ...
approach1 = brainS +>> robotC
or as a server it will fit into a push-based stream with a brain client:
brainC :: Color -> Client (Color,Turn) Color Identity ()
brainC = ...
approach2 = robotS >>~ brainC
来源:https://stackoverflow.com/questions/59333006/using-request-and-response-in-with-the-pipes-library-for-bidirectional-communica