1

Please help me to find a correct way to approach this task in Haskell.

Suppose, we want to write a simple server loop that will receive commands (requests) serialized in some way (as Strings, for simplicity of question), execute them and return serialized response back.

Let's start with data types that will contain request/response information:

data GetSomeStatsRequest = GetSomeStatsRequest
data GetSomeStatsResponse = GetSomeStatsResponse
    { worktime :: Int, cpuavg :: Int }

data DeleteImportantFileRequest = DeleteImportantFileRequest
    { filename :: String }
data DeleteImportantFileResponse = FileDeleted
                                 | CantDeleteTooImportant

data CalcSumOfNumbersRequest = CalcSumOfNumbersRequest Int Int
data CalcSumOfNumbersResponse = CalcSumOfNumbersResponse Int

Actual number of request types can be very large (hundreds) and has to be constantly maintained. Ideally we want requests independent from each other and organized into different modules. Because of that, joining them in one datatype (data Request = RequestA Int | RequestB String | ...) is not very practical. Same for responses.

But we are sure, that every request type has a unique response type, and we want to enforce this knowledge on compilation level. Class types with functional deps give us just that, am I right?

class Response b => Request a b | a -> b where
    processRequest :: a -> IO b
class Response b where
    serializeResponse :: b -> String

instance Request GetSomeStatsRequest GetSomeStatsResponse where
    processRequest req = return $ GetSomeStatsResponse 33 42
instance Response GetSomeStatsResponse where
    serializeResponse (GetSomeStatsResponse wt ca) =
        show wt ++ ", " ++ show ca

instance Request DeleteImportantFileRequest
                 DeleteImportantFileResponse where
    processRequest _ = return FileDeleted -- just pretending!
instance Response DeleteImportantFileResponse where
    serializeResponse FileDeleted = "done!"
    serializeResponse CantDeleteTooImportant = "nope!"

instance Request CalcSumOfNumbersRequest CalcSumOfNumbersResponse where
    processRequest (CalcSumOfNumbersRequest a b) =
        return $ CalcSumOfNumbersResponse (a + b)
instance Response CalcSumOfNumbersResponse where
    serializeResponse (CalcSumOfNumbersResponse r) = show r

Now, for the part that is tricky for me: main loop of our server... I imagine it should look somewhat like this (working with stdin/stdout for simplicity):

main :: IO ()
main = forever $ do
    putStrLn "Please enter your command!"
    cmdstr <- getLine
    let req = deserializeAnyRequest cmdstr
    resp <- processRequest req
    putStrLn $ "Result: " ++ (serializeResponse resp)

and function to deserialize any request:

deserializeAnyRequest :: Request a b => String -> a
deserializeAnyRequest str
    | head ws == "stats" = GetSomeStatsRequest
    | head ws == "delete" = DeleteImportantFileRequest (ws!!1)
    | head ws == "sum" = CalcSumOfNumbersRequest (read $ ws!!1) (read $ ws!!2)
    where ws = words str

Obviously, this fails to compile with Couldn't match expected type ‘a’ with actual type ‘CalcSumOfNumbersRequest’. And even if I would be able to create decodeAnyRequest function with this type signature, compiler will get confused in main function, because it will not know the actual type of req value (only typeclass restriction).

I understand, that my approach to this task is completely wrong. What is the best practical way to write request-response processor of this kind in Haskell?

Here is a Gist with example code combined: https://gist.github.com/anonymous/3ef9a0d0bd039b23c669

8
  • there are several ways - either you put all your commands into a ADT and have deserializeAnyRequest :: String -> MyCommand or you write tryDeserializeSpecificRequest :: String -> Maybe SpecificCommand and then try them one by one till you get a Some foundCommand... Commented Mar 12, 2016 at 16:26
  • Yep, basically, both methods are almost the same as @danidiaz suggestion, and right now, I'm thinking that this is the only possible approach... I don't like it because of code duplication and no compile-time checking of request-response types... Commented Mar 12, 2016 at 17:09
  • you get compile-time checking with the first - ofc. the actual parsing will always be runtime-checked ... obviously Commented Mar 12, 2016 at 17:16
  • 1
    As a note, if you really wanted to ensure a 1-1 connection between request and response types, you'd use a functional dependency like ... | a -> b, b -> a Commented Mar 12, 2016 at 22:04
  • 1
    @DerekElkins I guess I could imagine a situation where many requests had the same kind of response; e.g. DeleteFile and DeletePerson might both have DeletionResult as their response type. Commented Mar 12, 2016 at 22:31

3 Answers 3

3

Probably the most elegant way of solving this problem is servant. If servant applies to your situation, you should just use it. If not, the core idea in servant is easy to copy. Frankly, the route you started down is roughly in this direction, and it would eventually lead to this place if taken aggressively enough. There are a couple of issues with it currently.

The first issue is it's more important to capture deserializeAnyRequest in a type class than to capture serializeResponse in a type class. If I have a function A -> B, it's much easier for me to make that into A -> String, discarding type information, given that I know exactly what B is, than to turn it into String -> B which requires recovering type information. The trick is instead of looking at the data to find out what to do, we decide what to do (based on type) and then look at the data to verify that it's what we expect. This is how read, and, less trivially, printf and servant work. So make a type class for deserialization similar to Read.

The next issue is modularity. As you noticed, if we're going to drive our code by type, at some point we're going to need a type to describe all our input. We don't want to declare some giant type for this, at least not all at once. The solution is straightforward and simple: we make a type combinator to combine two types, and then we merely need to combine deserializers somehow and dispatch on the combined result. At a basic level, Either works perfectly fine for this.

Below is a very minimalistic code example illustrating the ideas above. I didn't include an equivalent to serializeResponse, as it's not necessary, but there's nothing wrong with keeping it. The deserializers are just String -> Maybe a functions. This is simple but inefficient. You could readily provide more information at the type level or return a less opaque type that would allow deserializers to be combined efficiently. See servant, for example. The only thing that needs to change to add a new request type (beyond writing the new code) is AppType, and that type can be a combination of type synonyms for subsets of the interface. Note that this approach doesn't preclude you from using a less typed (and more flexible) approach that, e.g., allows adding to request handlers at run-time. You just have a GenericRequest type.

You may also want to look at the finally, tagless approach for a more structured way of handling the request types.

module Main where

class Deserialize a where
    deserialize :: String -> Maybe a

class DoSomething a where
    doSomething :: a -> IO ()

data RequestA = RequestA Int deriving (Read)
instance Deserialize RequestA where
    deserialize s = case reads s of [(a,_)] -> Just a; _ -> Nothing
instance DoSomething RequestA where
    doSomething (RequestA i) = print i

data RequestB = RequestB Bool Bool deriving (Read)
instance Deserialize RequestB where
    deserialize s = case reads s of [(a,_)] -> Just a; _ -> Nothing
instance DoSomething RequestB where
    doSomething (RequestB a b) = print (a && b)

instance (Deserialize a, Deserialize b) => Deserialize (Either a b) where
    deserialize s = case deserialize s of
                        Just a -> Just (Left a)
                        Nothing -> case deserialize s of
                                        Just b -> Just (Right b)
                                        Nothing -> Nothing

instance (DoSomething a, DoSomething b) => DoSomething (Either a b) where
    doSomething (Left a) = doSomething a
    doSomething (Right b) = doSomething b

type AppType = Either RequestA RequestB

main = do
    i <- getLine
    case deserialize i :: Maybe AppType of
        Just a -> doSomething a
        Nothing -> putStrLn "Bad Input"

I should emphasize that this is really minimalistic. More realistically you'll probably want more context provided, most likely via a monad. For example, if you did want to do that GenericRequest thing, you'd probably want to have the deserializer initialized from a map of request handlers, but there is no way to provide that to the deserialize function. deserialize returning its result in a monad or taking an extra "context" parameter (which is a special case of the former) would enable that.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for detailed and well-considered response. Servant library doesn't fit my case exactly (I'm implementing hardware-level communication protocol), but I'll certainly will take a detailed look in its implementation. I have accepted Daniel's answer because I can apply it to the described problem more easily, but I definitely got XP from yours. Here is what I came up with for now (please let me know if you see any obvious mistakes in it): gist.github.com/anonymous/382e2c0411dce97b6040
2

The standard way is to use an existential type:

{-# LANGUAGE GADTs #-}
import Control.Applicative
import Text.Read

data ARequest where
    ARequest :: Request a b => a -> ARequest

You would then write a parser (which would be sort of non-modular in that it would have to know about all the instances of Request that you care about and be able to parse them all), and the responder would be implementable in terms of existing methods:

parse :: [String] -> Maybe ARequest
parse ["stats"]        = ARequest <$> pure   GetSomeStatsRequest
parse ["delete", file] = ARequest <$> liftA  DeleteImportantFileRequest (parseFileName file)
parse ["sum", a, b]    = ARequest <$> liftA2 CalcSumOfNumbersRequest (readMaybe a) (readMaybe b)
parse _ = empty

respond :: ARequest -> IO String
respond (ARequest r) = serializeResponse <$> processRequest r

3 Comments

Thanks! This seems like exactly what I needed... Especially "existential type" word combination to google :) I took your approach a bit further and made it modal. In case if someone will find this question and will be interested in the resulting code, here it is (finally compiling!): gist.github.com/anonymous/382e2c0411dce97b6040
@SergeyMitskevich The issue with this, and a common issue with existentials, is the existentials aren't helping much. From your gist, SomeResponse is basically the same thing as a string, and SomeRequest is basically the same thing as (String, IO String). When I crack open SomeRequest almost the only thing I can do with what's inside is apply processRequest, and almost the only thing I can do to the result of that is apply serializeResponse.
Isn't this exactly what is needed in this case? This way we ensure, that if we don't know which request is parsed, we can't do anything with it, and if we know, then we know the response type as well (the compiler knows). Module may not even export SomeRequest constructor at all, so no pattern matching can be done... Or am I missing something?
1

How about having, for each request/response pair, a function like:

type SerializedRequest = String

type SerializedResponse = String

parseSerializedRequest :: SerializedRequest -> Maybe (IO SerializedResponse)

The actual request/response types hide behind the function. We would have a list of these functions, assembled from each of your modules.

Whenever a serialized request arrives, we try each function in sequence until we find a match. The match returns the IO action to perform, along with the serialized response. If more than one action were possible, we can return them in a record.

4 Comments

Do I understand correctly, that in the end we'll have processAnyRequest :: String -> Maybe (IO String) function that will try all those? What if user will call that function, giving DeleteFileRequest, but expecting RenameFileResponse (wrongly, but in the end both of those will be Strings)? Run-time error? Is there a way to prevent this during compilation? Also, there will be hundreds of requests, this function will, basically duplicate list of all parse functions. What if some programmer on this project will add new request, but forget to add a line in this function? Run-time error, again?
"giving DeleteFileRequest, but expecting RenameFileResponse" Perhaps the client should not decide what response type to deserialize for each request, instead having a family of ParticularRequest -> IO ParticularResponse functions that handle the remote call and response deserialization for him.
Please ignore the first point then, I had in mind situation like in @Carsten comment. Ok, but what if we now want a function processAnyRequestAndPrettyPrintItToLog ? I mean it just as an example. We may need to do something with the request information (not serialized request, but actual data), we can't lose it. Should we write a separate function for each case and list all our separate parseSerializedRequest methods in each?
Perhaps parseSerializedRequest could return a record with an additional field, an IO action that pretty-printed the request. In this approach, we are forced to anticipate all required operations on requests/responses, to provide a common interface.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.