【问题标题】: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 多个命令,其中大部分可以在两种上下文中使用。这会导致大量重复。

标签: haskell types


【解决方案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类中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-07-18
        • 1970-01-01
        • 2020-06-02
        • 2022-01-09
        • 1970-01-01
        • 2022-11-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多