Using request and response in with the Pipes library for bidirectional communication

白昼怎懂夜的黑 提交于 2019-12-23 12:37:30

问题


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?


回答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 (Producers, Pipes and Consumers).

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!