仅适用于一种类型的一个构造函数的函数

Functions that only work with one constructor of a type

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

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

{-# 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] 元数据。有没有更好的方法在类型和值级别强制执行此操作?

如何使 Queue 成为一个普通的多态类型

data Queue a = Queue Name a

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

中进行模式匹配

如果除此之外,您需要可以在任何 Queue 中工作的函数,也许您可​​以在 DirectKeyTopicKey 的类型类中定义一些常用操作实例,然后有像

这样的签名
commonFunction :: MyTypeclass a => Queue a -> IO ()

也许你可以将这些函数直接放在类型类中

class MyTypeclass a where
    commonFunction :: Queue a -> IO ()

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

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

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

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

您正在通过类型变量(称为 kind)参数化 Queue 类型,允许您在类型级别通过哪种 [=20] 标记 Queue =] 你想参与其中。但是只有构造函数 Queue Name QueueType 根本没有引用 kind;这是幻影类型。 QueueType 插槽可以由任何有效的队列类型填充,而不管队列的 Queue kind 类型中的 kind 是什么。

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

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

具体而言,我们可以为您的队列创建一个类型,这样 Queue 'Direct 可以 包含直接队列类型,而 Queue 'Topic 可以 包含主题队列类型,您可以通过多态接受Queue a.

来处理其中任何一个

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

data QueueType
  = Direct
  | Topic

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

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

然后有一个类似标记为 Queue 的类型很简单 所代表的队列类型:

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

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

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

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

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

最后,当在 publish 函数中使用 Queue 'Direct 时,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