请帮助我找到在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
答案 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
操作以及序列化响应。如果可能有多个动作,我们可以将它们记录下来。