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
deserializeAnyRequest :: String -> MyCommandor you writetryDeserializeSpecificRequest :: String -> Maybe SpecificCommandand then try them one by one till you get aSome foundCommand...... | a -> b, b -> aDeleteFileandDeletePersonmight both haveDeletionResultas their response type.