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

时间:2011-12-08 17:12:19

标签: haskell types

我正在玩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是多态的,例如,根据上下文,可以返回StringByteString

以下代码应阐明上述文字。

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

实际问题

我的问题是,我如何编写一个在两个上下文中都有效的函数get,并使用适合上下文的返回类型类。我需要的是

  • 一种给get返回类型“Redis或RedisTransaction”的方法,

  • 类型aRedisValue上下文中Redis的实例,RedisStatus上下文中的RedisTransaction实例。< / p>

  • 一个函数decode,根据上下文自动执行正确的操作。我认为这必须来自(多参数)类型。

如果您知道我该怎么做或者有一些示例代码甚至文章的指针,那么您将得到我的谢意!

3 个答案:

答案 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都需要有足够的信息来唯一地确定ma

答案 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)

请注意ValueStatus类型同构词的使用:它会使您String的实现所产生的sendCommand更加强大。显然不仅仅是任意字符序列,而是遵循一些固定格式的返回值和状态。

答案 2 :(得分:4)

请记住,根据上下文,类型没有什么特别之处 - 这种情况一直伴随着类型推断。 []的类型为[a],但当您在True : []之类的内容中使用该类型时,该类型将专门针对上下文中的[Bool]

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

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

  
      
  • 一种给get返回类型“Redis或RedisTransaction”的方法,
  •   

这只需要get类型签名中的变量,例如get :: Key -> f af将以RedisRedisTransaction填写,具体取决于具体情况。

  
      
  • 类型aRedisValue上下文中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课程。