Search code examples
haskellhaskell-pipes

How can I have a pipe with multiple communication types?


Say I have this code:

import Control.Monad.State hiding (StateT)
import Control.Proxy

server :: (Proxy p, Monad m) => Int -> Server p Int Bool (StateT Int m) ()
server = runIdentityK loop
    where loop arg = do
        currMax <- lift get
        lift $ put $ max currMax arg
        nextArg <- respond (even arg)
        loop nextArg

client :: (Proxy p, Monad m) => Client p Int Bool m ()
client = runIdentityP loop
    where loop = go 1
          go i = do
            isEven <- request i
            go $ if isEven
                then i `div` 2
                else i * 3 + 1

Currently the client always sends Int, and receives Bool. However, I want the client to also be able to query for the highest value that the server has seen so far. So I also need communication of sending () and receiving Int. I could encode this as the client sending Either Int (), and receiving Either Bool Int. However, I'd like to ensure that the two aren't mixed - sending an Int always gets a Bool response.

How can this be done?


Solution

  • Any time you want a pipeline to have two separate interfaces you have to nest the Proxy monad transformer within itself. That means you want the type:

    twoInterfaces
        :: (Monad m, Proxy p1, Proxy p2 )
        => () -> Int -> Server p1 Int Bool (Server p2 () Int m) r
    twoInterfaces () n = runIdentityP . hoist runIdentityP $ do
        x <- respond A        -- Use outer interface
        y <- lift $ respond B -- Use inner interface
        ...
    

    Given the following two clients for each interface:

    client1 :: (Monad m, Proxy p) => () -> Client p Int Bool m r
    client2 :: (Monad m, Proxy p) => () -> Client p ()  Int  m r
    

    You would connect them to the two server interfaces using:

    oneInterface () = runProxy (twoInterfaces () >-> client1)
    
    main = runProxy (client1 >-> oneInterface)
    

    To learn more about this trick, read the Branching, zips, and merges section of the current tutorial.

    You can also do it the other way around, too. You can have a Client with two separate interface and hook up two different Servers. This may or may not fit your problem better.

    Note that this will get much simpler in pipes-4.0.0 (currently on Github), where the types will be much more concise and you won't need runIdentityP:

    twoInterfaces
        :: (Monad m) => () -> Int -> Server Int Bool (Server () Int m) r
    twoInterface () n = do
        x <- respond A
        y <- lift $ respond B
       ...
    
    client1 :: (Monad m) => () -> Client Int Bool m r
    client2 :: (Monad m) => () -> Client ()  Int  m r