6

我正在为消息队列编写一个库。队列可以是DirectTopicDirect队列有一个静态绑定键,而Topic队列可以有动态键。

我想编写一个publish仅适用于Direct队列的函数。这有效:

{-# LANGUAGE DataKinds #-}

type Name = Text
type DirectKey = Text
type TopicKey = [Text]

data QueueType
  = Direct DirectKey
  | Topic TopicKey

data Queue (kind :: a -> QueueType)
  = Queue Name QueueType

这需要两个单独的构造函数

directQueue :: Name -> DirectKey -> Queue 'Direct

topicQueue :: Name -> TopicKey -> Queue 'Topic

但是当我去写发布时,我需要匹配一个额外的模式,这应该是不可能的

publish :: Queue 'Direct -> IO ()
publish (Queue name (Direct key)) =
   doSomething name key
publish _ =
   error "should be impossible to get here"

有没有更好的方法来模拟这个问题,这样我就不需要模式匹配了?Direct队列应始终具有该Text元数据,并且Topic队列应始终具有该[Text]元数据。有没有更好的方法在类型和值级别上执行此操作?

4

2 回答 2

6

Queue制作一个普通的多态类型怎么样

data Queue a = Queue Name a

然后定义单独Queue DirectKey的和Queue TopicKey类型?那么你就不需要在publish :: Queue DirectKey -> IO ().

如果除此之外,您需要应该在 any 中工作的函数Queue,也许您可​​以在类型类中定义一些常见操作,其中DirectKeyTopicKey将是实例,然后具有类似的签名

commonFunction :: MyTypeclass a => Queue a -> IO ()

也许您可以将此类功能直接放在类型类中

class MyTypeclass a where
    commonFunction :: Queue a -> IO ()
于 2016-10-12T21:33:37.690 回答
3

您的代码没有按原样编译(它也需要PolyKinds打开)所以我不知道这是否只是一个意外,但看起来您正在尝试采用您从类型中知道的方法一个可能涉及构造函数的队列,因此可以静态保证只能在某种队列上调用函数。

实际上,您可以使用 GADT 的多个构造函数来使用该方法(而不是使用多个完全独立的类型,在必要时使用类型类将它们组合在一起,在@danidiaz 的回答中建议的方法)。

但首先为什么您当前的代码不起作用。在您的队列类型中:

data Queue (kind :: a -> QueueType)
  = Queue Name QueueType

您正在Queue通过类型变量(称为 )对类型进行参数化,从而允许您在类型级别kind标记 a您希望在其中的类型。但是只有构造函数根本没有引用;这是一种幻影类型。无论队列类型是什么,该插槽都可以由任何有效的队列类型填充。QueueQueueTypeQueue Name QueueTypekindQueueTypekindQueue kind

这意味着 GHC 希望您添加一个匹配;publish中的主题键的 case 是正确的。Queue 'Direct您的数据类型定义说可以存在这样的值。

GADT 允许您单独显式声明每个构造函数的完整类型,包括返回类型。因此,您可以在您正在构造的值的类型与可能用于生成该类型值的构造函数(或其参数)之间建立关系。

具体来说,我们可以为您的队列创建一个类型,使其Queue 'Direct只能包含直接队列类型,并且只能包含主题队列类型,并且您可以通过多态接受 a 来处理。Queue 'TopicQueue a

最简单的做法是QueueType 用于标签,并有一个单独的 GADT 存储数据。在您的原始代码中,您可以重用提升到类型级别且未应用的数据保存构造函数,但这会使您的类型签名变得不必要地复杂(引入对 的需要PolyKinds),并且如果您需要添加更多(以及不同数量的!) 数据构造函数的参数,当提升到类型级别时,将其未应用的类型硬塞起来以适应相同的类型将变得越来越困难。所以:

data QueueType
  = Direct
  | Topic

data QueueData (a :: QueueType)
  where DirectData :: DirectKey -> QueueData 'Direct
        TopicData :: TopicKey -> QueueData 'Topic

所以我们QueueType只需要提升DataKinds(通常不需要在值级别实际使用这种类型)。然后我们得到了QueueData由 type-level 参数化的类型QueueType。一个构造函数接受 aDirectKey并构造 a QueueData 'Direct,另一个接受 aTopicKey并构造 a QueueData 'Topic

然后很容易拥有一个Queue类似地用所表示的队列类型标记的类型:

data Queue (a :: QueueType)
  = Queue Name (QueueData a)

现在,如果一个函数在任何队列上工作(比如说因为它只需要访问 之外的名称QueueData),它可以采用Queue a

getName :: Queue a -> Text
getName (Queue name _) = name

如果您可以明确处理所有案例,您也可以采取 a Queue a,当您错过案例时,您会收到警告:

getKeyText :: Queue a -> Text
getKeyText (Queue _ (DirectData key)) = key
getKeyText (Queue _ (TopicData keys)) = mconcat keys

最后,当Queue 'Direct在你的publish函数中使用 as 时,GHC 知道这DirectDataQueueData. 因此,您不需要像在 OP 中那样添加错误案例,如果您尝试TopicData在那里处理内部,它实际上会被检测为类型错误。

完整示例:

{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

import Data.Text (Text)

type Name = Text
type DirectKey = Text
type TopicKey = [Text]

data QueueType
  = Direct
  | Topic

data QueueData (a :: QueueType)
  where DirectData :: DirectKey -> QueueData 'Direct
        TopicData :: TopicKey -> QueueData 'Topic

data Queue (a :: QueueType)
  = Queue Name (QueueData a)


getName :: Queue a -> Text
getName (Queue name _) = name

getKeyText :: Queue a -> Text
getKeyText (Queue _ (DirectData key)) = key
getKeyText (Queue _ (TopicData keys)) = mconcat keys

publish :: Queue 'Direct -> IO ()
publish (Queue name (DirectData key))
  = doSomething name key
  where doSomething = undefined
于 2016-10-13T01:05:47.767 回答