Up to Index

Representing an RPC API

The Bank

Say I want a remote API for my awesome Bank:

balance :: IORef Int -> IO Int
balance ref = readIORef ref

withdraw :: IORef Int -> Int -> IO Bool
withdraw ref ammount =
    b <- balance ref
    if ammount <= b then
      modifyIORef ref (+ (-ammount)) >> return True
      return False
(deposit is not needed, everybody only ever withdraws anyway)

Both the client and the server will be written in haskell, so it would be nice to define the API as a datatype, so that I may use this type in both server and client.

My first instinct is to represent the bank API using a sum type:

data APICall =
  | Withdraw Int
Then I derive JSON or something for it and send/receive over a socket. I could then make a type representing responses:
data APIResponse Int
    BalanceResponse Int
  | WithdrawResponse Bool
Now I can send/receive that as well.

But using this scheme, nothing stops me from responding to a Balance with a WithdrawResponse or something like that. I would like the type of the call and the type of the response to be tied together somehow. This is my attempt.


I put the return type in the API via a function that takes the return type and return some other final return type, r.

data API r =
    Balance (Int -> r)
  | Withdraw Int (Bool -> r)
Now the return type of Balance is tied to that constructor. (But functions can't be serialized you say? I'll cheat, explanation later)

The Server

But what does the (Int -> r) and (Bool -> r) functions mean? They are how to respond to the call, so that for the server side, I can write a dispatch function like so:

dispatch :: IORef Int -> API r -> IO r
dispatch ref (Balance respond) = fmap respond $ balance ref
dispatch ref (Withdraw ammount respond) = fmap respond $ withdraw ref ammount
I want to communicate over a socket, so I could make the type parameter r = (Handle -> IO ()), so given a handle, it will send the response:
serveAPI :: Handle -> IORef Int -> API (Handle -> IO ()) -> IO ()
serveAPI h ref api = dispatch ref api >>= ($ h)

I'll use Read/Show to deserialize/serialize my API calls/responses, as one would in a production setting. My server then becomes:

serve :: Handle -> IORef Int -> IO ()
serve h ref = forever $ hGetLine h >>= serveAPI h ref . read
But at this point I need a Read instance for API, or more specifically for API (Handle -> IO ()). I can use StandaloneDeriving for this:
deriving instance Read (API (Handle -> IO ()))
Now GHC complains about there being no Read instance for Bool -> Handle -> IO (), and there's no way to serialize functions. But I don't need to, the respond function will allways do the same thing for a given API call; it will show the return value, and write it to a handle:
instance Show a => Read (a -> Handle -> IO ()) where
  readsPrec _ s = [(\x h -> hPutStrLn h $ show x,s)]

The Client

So what does the respond function do on the client side? Here it just a way to get a return value to the caller:

send :: Show (API (IO r)) => Handle -> (API (IO r)) -> IO r
send h api =
    hPutStrLn h $ show api
    l <- hGetLine h
    case api of
      Balance respond -> respond $ read l
      Withdraw _ respond -> respond $ read l
Using a convenient call function I can make calls to the API in a natural way, and get back the right return value:
call :: Show (API (IO r)) => Handle -> ((r -> IO r) -> API (IO r)) -> IO r
call h f = send h (f return)
So for example:
*API> :t call undefined Balance
call undefined Balance :: IO Int
*API> :t call undefined $ Withdraw 10
call undefined $ Withdraw 10 :: IO Bool

To wrap up I need a Show instance for API:

instance Show (a -> IO b) where
  show _ = ""

deriving instance Show (API (IO a))
In order to test our API I need a rudimentary server that listens on a socket:
runServer :: PortNumber -> Int -> IO ()
runServer port init =
    ref <- newIORef init
    listen <- listenOn $ PortNumber port
    (h,_,_) <- accept listen
    serve h ref
Now I can try it out in GHCi

Start two ghci sessions and load API.hs in both. In the first one run:

*API> runServer 8888 100
In the second, open a socket to the server:
*API> h <- connectTo "localhost" $ PortNumber 8888
Now I can try out some calls:
*API> call h Balance
*API> call h $ Withdraw 20
*API> call h $ Withdraw 100
*API> call h Balance

The End

This does feel a bit like a hack, especially the way Read and Show are treated. On the other hand it does what I want it to do, and it doesn't use any "advanced" GHC features(the language extensions are only used in order to derive Show and Read instances and could have been removed if I had written the instances by hand like a real man). In fact it should be perfectly possible to use this pattern in Standard ML/O'Caml.

jon.petter.bergman@gmail.com 2016-03-22.