Haskell中的请求 - 响应处理器

时间:2016-03-12 15:48:46

标签: haskell

请帮助我找到在Haskell中处理此任务的正确方法。

假设我们想编写一个简单的服务器循环,它将接收以某种方式序列化的命令(请求)(为了简单起见,作为字符串),执行它们并返回序列化响应。

让我们从包含请求/响应信息的数据类型开始:

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

请求类型的实际数量可能非常大(数百个),必须不断维护。理想情况下,我们希望请求彼此独立并组织成不同的模块。因此,将它们连接成一种数据类型(data Request = RequestA Int | RequestB String | ...)并不是很实用。同样的回应。

但我们确信,每个请求类型都有唯一的响应类型,我们希望在编译级别上强制执行此知识。具有功能性deps的类类型给我们提供了这些,我是对的吗?

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

现在,对于对我来说棘手的部分:我们服务器的主循环...我想它应该看起来像这样(为了简单起见使用stdin / stdout):

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

并用于反序列化任何请求:

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

显然,这无法使用Couldn't match expected type ‘a’ with actual type ‘CalcSumOfNumbersRequest’进行编译。即使我能够使用此类型签名创建decodeAnyRequest函数,编译器也会在main函数中混淆,因为它不会知道req值的实际类型(仅限类型类限制)。 / p>

我明白,我对这项任务的态度是完全错误的。 在Haskell中编写此类请求 - 响应处理器的最佳实用方法是什么?

以下是包含示例代码的Gist:https://gist.github.com/anonymous/3ef9a0d0bd039b23c669

3 个答案:

答案 0 :(得分:3)

解决这个问题最优雅的方法可能是servant。如果仆人适用于您的情况,您应该只使用它。如果没有,仆人的核心思想很容易复制。坦率地说,你开始下行的路线大致是朝着这个方向发展的,如果采取足够积极的态度,它最终会导致这个地方。目前有几个问题。

第一个问题是在类型类中捕获deserializeAnyRequest比在类型类中捕获serializeResponse更重要。如果我有一个函数A -> B,那么我可以更轻松地将其转换为A -> String,放弃类型信息,因为我确切知道B是什么,而不是将其转换为{{ 1}}需要恢复类型信息。诀窍不是查看数据以找出要做的事情,而是根据类型决定做什么,然后查看数据以验证它是我们期望的。这就是String -> B,而不是简单地read和仆人的工作方式。因此,为反序列化创建一个类型类,类似于printf

下一个问题是模块化。正如您所注意到的,如果我们要按类型驱动我们的代码,在某些时候我们将需要一个类型来描述我们所有的输入。我们不想为此声明一些巨型,至少不是一次性。解决方案简单明了:我们组建一个类型组合器来组合两种类型,然后我们只需要以某种方式组合反序列化器并对组合结果进行调度。在基本级别,Read对此非常好。

以下是一个非常简约的代码示例,说明了上述想法。我没有包含等同于Either的内容,因为它没有必要,但保留它并没有错。反序列化器只是serializeResponse函数。这很简单但效率低下。您可以在类型级别提供更多信息,或者返回一个不太透明的类型,以便有效地组合反序列化器。例如,见仆人。除了编写新代码之外,唯一需要更改以添加新请求类型的是String -> Maybe a,并且该类型可以是接口子集的类型同义词的组合。请注意,此方法不会排除您使用类型较少(且更灵活)的方法,例如,允许在运行时添加到请求处理程序。您只有AppType类型。

您可能还希望查看finally, tagless approach以获得更加结构化的方式来处理请求类型。

GenericRequest

我应该强调这是真的简约。更现实的是,您可能希望提供更多上下文,最有可能通过monad。例如,如果您确实想要执行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" 事件,您可能希望从请求处理程序的映射中初始化反序列化程序,但是无法将其提供给GenericRequest函数。 deserialize将结果返回monad或者使用额外的“context”参数(这是前者的特殊情况)可以实现。

答案 1 :(得分:2)

标准方法是使用存在类型:

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

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

然后你会编写一个解析器(它将是一种非模块化的,因为它必须知道你关心的Request的所有实例,并且能够解析所有这些实例),以及响应者可以在现有方法中实现:

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

答案 2 :(得分:1)

对于每个请求/响应对,如何使用以下函数:

type SerializedRequest = String

type SerializedResponse = String

parseSerializedRequest :: SerializedRequest -> Maybe (IO SerializedResponse)

实际的请求/响应类型隐藏在函数后面。我们将列出这些函数,从每个模块组装。

每当序列化请求到达时,我们按顺序尝试每个函数,直到找到匹配项。该匹配返回要执行的IO操作以及序列化响应。如果可能有多个动作,我们可以将它们记录下来。