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

时间:2016-10-12 21:00:35

标签: haskell types message-queue data-kinds

我正在为消息队列编写一个lib。队列可以是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]个元数据。是否有更好的方法在类型和价值级别强制执行此操作?

2 个答案:

答案 0 :(得分:6)

如何使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 ()

答案 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知道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