我正在为消息队列编写一个lib。队列可以是Direct
或Topic
。 Direct
个队列具有静态绑定密钥,而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]
个元数据。是否有更好的方法在类型和价值级别强制执行此操作?
答案 0 :(得分:6)
如何使Queue
成为普通的多态类型
data Queue a = Queue Name a
然后定义单独的Queue DirectKey
和Queue TopicKey
类型?那么你就不需要在publish :: Queue DirectKey -> IO ()
中进行模式匹配。
除此之外,如果您需要在任何Queue
中都有效的函数,也许您可以在类别类中定义一些常见操作,其中DirectKey
和TopicKey
将是实例,然后有像
commonFunction :: MyTypeclass a => Queue a -> IO ()
也许你可以将这些函数直接放在类型类
中class MyTypeclass a where
commonFunction :: Queue a -> IO ()
答案 1 :(得分:3)
您的代码没有按原样编译(它也需要启用viewDidLoad()
)所以我不知道它是否只是一次意外,但它看起来就像你试图从你可以参与构造函数的队列类型那样知道的方法,因此可以静态地保证只能在某种队列上调用函数。
事实上,您可以使用GADT的多个构造函数(而不是使用多个完全独立的类型,而不是使用类型类在必要时将它们组合在一起),使用@ danidiaz'中提出的方法。回答)。
但首先为什么你当前的代码不起作用。在您的队列中输入:
PolyKinds
您通过类型变量(称为data Queue (kind :: a -> QueueType)
= Queue Name QueueType
)对Queue
类型进行参数设置,允许您在类型级别标记kind
{{1}你想进入它。但只有构造函数Queue
根本没有引用QueueType
;它是幻影类型。无论Queue Name QueueType
类型的队列kind
是什么QueueType
,都可以使用任何有效的队列类型填充kind
个插槽。
这意味着GHC在您希望Queue kind
添加与publish
内的主题键匹配的案例时是正确的。您的数据类型定义表明这些值可以存在。
GADT允许您做的是分别显式声明每个构造函数的完整类型,包括返回类型。因此,您可以在您构建的值的类型与可能用于生成该类型值的构造函数(或其参数)之间建立关系。
具体而言,我们可以为您的队列创建一个类型,以便Queue 'Direct
只能 包含直接队列类型,而Queue 'Direct
只能 包含主题队列类型,您可以通过多态接受Queue 'Topic
来处理。
最简单的方法是将Queue a
用于标记,并使用单独的GADT存储数据。在您的原始代码中,您可以重用已提升到类型级别且未应用的数据保持构造函数,但这会使您的类型签名不必要地复杂化(需要QueueType
),并且如果您需要添加更多(以及不同数量的!)参数对于数据构造函数来说,当提升到类型级别时,将它们未应用的类型用于装配相同类型将变得越来越困难。所以:
PolyKinds
所以我们data QueueType
= Direct
| Topic
data QueueData (a :: QueueType)
where DirectData :: DirectKey -> QueueData 'Direct
TopicData :: TopicKey -> QueueData 'Topic
只是QueueType
来抬起DataKinds
(通常不需要在价值级别实际使用这种类型)。然后,我们通过类型级QueueData
获取了QueueType
类型的参数。一个构造函数接受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
最后,当您在Queue 'Direct
函数中使用publish
时,GHC知道DirectData
是QueueData
唯一可能的构造函数。因此,您不需要像在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