我正在玩Haskell中实现Redis客户端库,我的目标是尽可能地编码Haskell类型系统中Redis命令的语义。对于那些不知道的人来说,Redis是一个通过网络访问的数据存储区。我会用它来举例说明我的问题,但Redis并不是这个问题的焦点。
考虑功能
get :: (RedisValue a) => Key -> Redis a
get k = decodeValue <$> sendCommand ["GET", key]
它向数据存储区发送命令并返回存储在给定Key
下的值(对于此示例,您可以考虑type Key = String
)。至于返回类型:
Redis
是Monad
和MonadIO
的一个实例。它封装了有关网络连接的信息。 sendCommand
发送请求并返回数据存储区的回复。
a
是多态的,例如,根据上下文,可以返回String
或ByteString
。
以下代码应阐明上述文字。
data Redis a = ...
instance MonadIO Redis where ...
instance Monad Redis where ...
sendCommand :: [String] -> Redis String
class RedisValue a where
decodeValue :: String -> a
-- example instances
instance RedisValue String where ...
instance RedisValue ByteString where ...
Redis支持简单的交易形式。在事务中,大多数命令可以与事务外部相同地发送。但是,它们的执行会延迟,直到用户发送提交命令(在Redis中称为exec
)。在事务内部,数据存储区仅返回存储命令以供以后执行的确认。提交(exec
)后,将返回所有存储命令的所有结果。
这意味着上面的get
- 函数在事务上下文中看起来有点不同:
get :: (RedisStatus a) => Key -> RedisTransaction a
get k = decodeStatus <$> sendCommand ["GET", key]
请注意:
monadic类型现在为RedisTransaction
,表示交易背景。
a
返回类型现在是RedisStatus
的任何实例。 RedisValue
和RedisStatus
的实例之间存在重叠。例如,String
在两个类中。专门的Status
数据类型可能只在RedisStatus
类中。
我的问题是,我如何编写一个在两个上下文中都有效的函数get
,并使用适合上下文的返回类型类。我需要的是
一种给get
返回类型“Redis或RedisTransaction”的方法,
类型a
是RedisValue
上下文中Redis
的实例,RedisStatus
上下文中的RedisTransaction
实例。< / p>
一个函数decode
,根据上下文自动执行正确的操作。我认为这必须来自(多参数)类型。
如果您知道我该怎么做或者有一些示例代码甚至文章的指针,那么您将得到我的谢意!
答案 0 :(得分:5)
首先,我认为最好有两个不同的get命令。也就是说,这是一种方法。
class RedisGet m a where
get :: Key -> m a
instance (RedisValue a) => RedisGet Redis a where...
instance (RedisStatus a) => RedisGet RedisTransaction a where...
你需要
MPTC,但没有FunDeps或Type Families。每次使用get都需要有足够的信息来唯一地确定m
和a
。
答案 1 :(得分:5)
我同意多参数类型类型非常适合这里。这是一种方法:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
newtype Redis a = Redis (IO a) deriving Monad
newtype RedisTransaction a = RedisTransaction (IO a) deriving Monad
newtype Key = Key {unKey :: String}
newtype Value = Value {unValue :: String}
newtype Status = Status {unStatus :: String}
class Monad m => RedisMonad m a | m -> a where
sendCommand :: [String] -> m a
instance RedisMonad Redis Value where
sendCommand = undefined -- TODO: provide implementation
instance RedisMonad RedisTransaction Status where
sendCommand = undefined -- TODO: provide implementation
class Decodable a b where
decode :: a -> b
instance Decodable Status String where
decode = unStatus
instance Decodable Value String where
decode = unValue
get :: (RedisMonad m a, Decodable a b) => Key -> m b
get k = do
response <- sendCommand ["GET", unKey k]
return (decode response)
请注意Value
和Status
类型同构词的使用:它会使您String
的实现所产生的sendCommand
更加强大。显然不仅仅是任意字符序列,而是遵循一些固定格式的返回值和状态。
答案 2 :(得分:4)
请记住,根据上下文,类型没有什么特别之处 - 这种情况一直伴随着类型推断。 []
的类型为[a]
,但当您在True : []
之类的内容中使用该类型时,该类型将专门针对上下文中的[Bool]
。
重要的是,如果您希望函数的实现或值的定义依赖于其类型。如果然后以正常方式从上下文推断出该类型,则最终会得到一个根据上下文执行“不同”操作的函数。依赖于类型的实现是使用类型类的主要目的。
现在,回答您的具体问题:
- 一种给
get
返回类型“Redis或RedisTransaction”的方法,
这只需要get
类型签名中的变量,例如get :: Key -> f a
。 f
将以Redis
或RedisTransaction
填写,具体取决于具体情况。
- 类型
a
是RedisValue
上下文中Redis
的实例,RedisStatus
上下文中的RedisTransaction
实例。
由于a
和上下文类型都是从使用中推断出来的,所以你在这里真正理解的是限制可能的类型,这相当于预期类型检查错误他们不匹配。这是类型类的另一个目的,可以在上下文类型变量上使用适当的类约束来实现:
get :: (ContextValue (f a)) => Key -> f a
class ContextValue a
instance (RedisValue a) => ContextValue (Redis a)
instance (RedisStatus a) => ContextValue (RedisTransaction a)
或类似的东西。但仅凭这一点对你的目的来说还不够,因为......
- 一个函数
decode
,根据上下文自动执行正确的操作。我认为这必须来自(多参数)类型。
这意味着根据类型选择decode
的实现,这意味着将其作为类型类的一部分,例如上面的ContextValue
。你如何处理这取决于decode
的类型需要是什么 - 如果结果类型需要像f String -> f a
那样f
是monadic context,那么你可能会需要更复杂的东西,比如在dblhelix的答案中。如果您只需要String -> f a
,那么您可以直接将其添加到上述ContextValue
课程。