取决于上下文的多态返回类型
Posted
技术标签:
【中文标题】取决于上下文的多态返回类型【英文标题】:Polymorphic Return Types Depending on Context 【发布时间】:2011-12-08 17:12:18 【问题描述】:我正在尝试在 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
s 或ByteString
s,具体取决于上下文。
下面的代码应该澄清上面的文字。
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]
注意:
一元类型现在是RedisTransaction
以指示事务上下文。
a
返回类型现在是RedisStatus
的任何实例。 RedisValue
和 RedisStatus
的实例之间存在重叠。例如String
在两个类中。专用的Status
数据类型可能仅在RedisStatus
类中。
实际问题
我的问题是,我如何编写一个函数get
,它可以在两种上下文中工作,并具有适合上下文的返回类型类。我需要的是
一种为get
提供返回类型“Redis 或 RedisTransaction”的方法,
类型 a
在Redis
上下文中是RedisValue
的实例,在RedisStatus
上下文中是RedisStatus
的实例。
一个函数decode
根据上下文自动做正确的事情。我认为这必须来自(多参数)类型类。
如果您知道我如何做到这一点,或者有一些示例代码甚至文章的指针,我将不胜感激!
【问题讨论】:
您能否创建两个不同的函数 - 一个用于Redis
上下文,一个用于RedisTransaction
上下文?
是否单个RedisValue
可以表示为String
或ByteString
,或者给定RedisValue
的RedisValue
是两者之一,但不是两者兼而有之?如果前者为真,@sclv 的解决方案应该可以工作,但后者需要一些额外的技巧。
另外,这是一个非常非常好的第一个 SO 问题。请留下来!
谢谢,acfoltzer。关于您的问题:单个 RedisValue 可以表示为 String 或 ByteString (甚至是其他东西)。
Cable729,是的,我可以。从可用性和新手友好的角度来看,这可能是最好的,因为两个独立的函数(及其类型)get
和 getTx
比组合函数更容易理解。但是,Redis 有 100 多个命令,其中大部分可以在两种上下文中使用。这会导致大量重复。
【参考方案1】:
首先,我认为最好有两个不同的 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
。
【讨论】:
【参考方案2】:我同意多参数类型类非常适合这里。这是一种方法:
-# 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
s 是由sendCommand
的实现产生的,显然不仅仅是任意字符序列而是遵循一些固定的返回值和状态格式。
【讨论】:
【参考方案3】:请记住,依赖于上下文的类型并没有什么特别之处——在类型推断中总是会发生这种情况。 []
的类型是 [a]
,但是当您在 True : []
之类的东西中使用它时,该类型将在上下文中专用于 [Bool]
。
重要的变化是您是否希望函数的实现或值的定义取决于其类型。如果然后以正常方式从上下文中推断出该类型,则最终会得到一个根据上下文执行“不同”操作的函数。依赖类型的实现是使用类型类的主要目的。
现在,回答您的具体问题:
一种为get
提供返回类型“Redis 或 RedisTransaction”的方法,
这仅需要get
类型签名中的变量,例如get :: Key -> f a
。 f
将根据上下文填写为 Redis
或 RedisTransaction
。
a
类型在Redis
上下文中是RedisValue
的实例,在RedisTransaction
上下文中是RedisStatus
的实例。
由于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
是一元上下文,那么你可能需要一些更详细的东西,就像在 dblhelix 的回答中一样。如果你只需要String -> f a
,那么你可以直接将它添加到上面的ContextValue
类中。
【讨论】:
以上是关于取决于上下文的多态返回类型的主要内容,如果未能解决你的问题,请参考以下文章
Python 多态 对象常用内置函数 运算符重载 对象迭代器 上下文管理
Python基础- 类和对象(使用继承派生组合接口多态封装propertystaticmethodclassmethod反射slots上下文管理协议元类)