取决于上下文的多态返回类型

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)。至于返回类型:

RedisMonadMonadIO 的一个实例。它封装了有关网络连接的信息。 sendCommand 发送请求并返回数据存储区的回复。

a 是多态的,例如可以返回Strings 或ByteStrings,具体取决于上下文。

下面的代码应该澄清上面的文字。

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 的任何实例。 RedisValueRedisStatus 的实例之间存在重叠。例如String 在两个类中。专用的Status 数据类型可能仅在RedisStatus 类中。

实际问题

我的问题是,我如何编写一个函数get,它可以在两种上下文中工作,并具有适合上下文的返回类型类。我需要的是

一种为get 提供返回类型“Redis 或 RedisTransaction”的方法,

类型 aRedis 上下文中是RedisValue 的实例,在RedisStatus 上下文中是RedisStatus 的实例。

一个函数decode 根据上下文自动做正确的事情。我认为这必须来自(多参数)类型类。

如果您知道我如何做到这一点,或者有一些示例代码甚至文章的指针,我将不胜感激!

【问题讨论】:

您能否创建两个不同的函数 - 一个用于Redis 上下文,一个用于RedisTransaction 上下文? 是否单个RedisValue 可以表示为StringByteString,或者给定RedisValueRedisValue 是两者之一,但不是两者兼而有之?如果前者为真,@sclv 的解决方案应该可以工作,但后者需要一些额外的技巧。 另外,这是一个非常非常好的第一个 SO 问题。请留下来! 谢谢,acfoltzer。关于您的问题:单个 RedisValue 可以表示为 String 或 ByteString (甚至是其他东西)。 Cable729,是的,我可以。从可用性和新手友好的角度来看,这可能是最好的,因为两个独立的函数(及其类型)getgetTx 比组合函数更容易理解。但是,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 都需要有足够的信息来唯一确定 ma

【讨论】:

【参考方案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)

注意ValueStatus 的类型同构的使用:它使事情的类型稍微强一些,因为Strings 是由sendCommand 的实现产生的,显然不仅仅是任意字符序列而是遵循一些固定的返回值和状态格式。

【讨论】:

【参考方案3】:

请记住,依赖于上下文的类型并没有什么特别之处——在类型推断中总是会发生这种情况。 [] 的类型是 [a],但是当您在 True : [] 之类的东西中使用它时,该类型将在上下文中专用于 [Bool]

重要的变化是您是否希望函数的实现或值的定义取决于其类型。如果然后以正常方式从上下文中推断出该类型,则最终会得到一个根据上下文执行“不同”操作的函数。依赖类型的实现是使用类型类的主要目的。

现在,回答您的具体问题:

一种为get 提供返回类型“Redis 或 RedisTransaction”的方法,

这仅需要get 类型签名中的变量,例如get :: Key -&gt; f af 将根据上下文填写为 RedisRedisTransaction

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 -&gt; f a 其中f 是一元上下文,那么你可能需要一些更详细的东西,就像在 dblhelix 的回答中一样。如果你只需要String -&gt; f a,那么你可以直接将它添加到上面的ContextValue类中。

【讨论】:

以上是关于取决于上下文的多态返回类型的主要内容,如果未能解决你的问题,请参考以下文章

[ C++ ] 抽象类 虚函数 虚函数表 -- C++多态

Python 多态 对象常用内置函数 运算符重载 对象迭代器 上下文管理

在此上下文中仅允许返回数字或布尔值的变量表达式

Java Script 什么是闭包?

Python基础- 类和对象(使用继承派生组合接口多态封装propertystaticmethodclassmethod反射slots上下文管理协议元类)

C++中的动态类型与动态绑定虚函数运行时多态的实现