19

是否可以创建一个不能再接纳新成员的类型类(也许通过使用模块边界)?我可以拒绝导出完整实例定义所必需的函数,但这只会在有人生成无效实例时导致运行时错误。我可以让它成为编译时错误吗?

4

6 回答 6

24

从 GHC 7.8.1 开始,可以声明封闭类型族ConstraintKinds,我认为在它们的帮助下,你可以这样做:

type family SecretClass (a :: *) :: Constraint where
  SecretClass Int = ()

SecretClass a形成一个约束,相当于一个类型类,并且由于该家族不能被任何人扩展,因此不能定义“类”的其他实例。

(这实际上只是猜测,因为我无法测试它,但这个有趣链接中的代码使它看起来可以工作。)

于 2013-07-25T09:51:53.377 回答
16

我相信答案是肯定的,这取决于您要实现的目标。

您可以避免从接口模块1导出类型类名称本身,同时仍导出类型类函数的名称。那么没有人可以创建该类的实例,因为没有人可以命名它!

例子:

module Foo (
    foo,
    bar
) where

class SecretClass a where
    foo :: a
    bar :: a -> a -> a

instance SecretClass Int where
    foo = 3
    bar = (+)

缺点是没有人可以用你的类作为约束来编写类型。这并不完全阻止人们编写具有这种类型的函数,因为编译器仍然能够推断出该类型。但这会很烦人。

您可以通过提供另一个空类型类来减轻不利影响,将“封闭”类作为超类。您使原始类的每个实例也成为子类的实例,并且导出子类(以及所有类型类函数),但不导出超类。(为了清楚起见,您可能应该在您公开的所有类型中使用“公共”类而不是“秘密”类,但我相信它可以使用任何一种方式)。

例子:

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

module Foo ( 
    PublicClass,
    foo,  
    bar   
) where 

class SecretClass a where 
    foo :: a
    bar :: a -> a -> a

class SecretClass a => PublicClass a

instance SecretClass Int where 
    foo = 3
    bar = (+) 

instance SecretClass a => PublicClass a

如果您愿意PublicClass为每个SecretClass.

现在客户端代码可以PublicClass用来编写类型类约束,但是每个实例都PublicClass需要一个SecretClass相同类型的实例,并且没有办法声明一个新的实例,SecretClass没有人可以创建更多的类型实例PublicClass2

所有这一切都没有让您获得编译器将类视为“关闭”的能力。它仍然会抱怨可以通过选择“已关闭”的唯一可见实例来解决的模棱两可的类型变量。


1纯意见:通常最好有一个单独的内部模块,其名称很吓人,可以导出所有内容,以便您可以使用它进行测试/调试,并使用一个接口模块导入内部模块并仅导出您想要的东西出口。

2我猜想通过扩展有人可以声明一个新的重叠实例。例如,如果您提供了一个 for 的实例[a],那么有人可以声明一个新的PublicClassfor实例[Int],这将搭载SecretClassfor的实例[a]。但鉴于它PublicClass没有任何功能,而且他们无法编写一个实例,SecretClass我看不出可以用它做多少事情。

于 2013-07-25T07:02:37.333 回答
15

您可以通过封闭类型族对封闭类型类进行编码,这些封闭类型族基本上可以依次编码为关联类型族。这个解决方案的关键是关联类型族的实例都在一个类型类实例中,并且每个单态类型只能有一个类型类实例。

请注意,这种方法独立于模块系统。我们不依赖于模块边界,而是提供了一个明确的列表,其中列出了哪些实例是合法的。这意味着,一方面,合法实例可以分布在多个模块甚至包中,另一方面,即使在同一个模块中,我们也不能提供非法实例。

对于这个答案,我假设我们要关闭以下类,以便它只能为类型Int和实例化Integer,而不能为其他类型实例化:

 -- not yet closed
class Example a where
  method :: a -> a

首先,我们需要一个将封闭类型族编码为关联类型族的小框架。

{-# LANGUAGE TypeFamilies, EmptyDataDecls #-}

class Closed c where
  type Instance c a

参数c代表类型族的名称,参数a是类型族的索引。cfor的族实例a被编码为Instance c a。由于c也是类参数,c因此必须在单个类实例声明中一起给出 的所有系列实例。

现在,我们使用这个框架来定义一个封闭的类型族MemberOfExample来编码IntIntegerOk,而所有其他类型都不是。

data MemberOfExample
data Ok

instance Closed MemberOfExample where
  type Instance MemberOfExample Int = Ok
  type Instance MemberOfExample Integer = Ok

最后,我们在Example.

class Instance MemberOfExample a ~ Ok => Example a where
  method :: a -> a

我们可以像往常一样定义有效Int的实例。Integer

instance Example Int where
  method x = x + 1

instance Example Integer where
  method x = x + 1

但是我们不能为Int和之外的其他类型定义无效实例Integer

-- GHC error: Couldn't match type `Instance MemberOfExample Float' with `Ok'
instance Example Float where
  method x = x + 1

我们也不能扩展有效类型的集合。

-- GHC error: Duplicate instance declarations
instance Closed MemberOfExample where
  type Instance MemberOfExample Float = Ok

-- GHC error: Associated type `Instance' must be inside a class instance
type instance Instance MemberOfExample Float = Ok

不幸的是,我们可以编写以下虚假实例:

-- Unfortunately accepted
instance Instance MemberOfExample Float ~ Ok => Example Float where
  method x = x + 1

但是由于我们永远无法解除平等约束,我认为我们永远无法将它用于任何事情。例如,以下内容被拒绝:

-- Couldn't match type `Instance MemberOfExample Float' with `Ok'
test = method (pi :: Float)
于 2013-07-25T16:34:43.720 回答
7

您可以将类型类重构为数据声明(使用记录语法),其中包含您的类型类具有的所有功能。一个固定的有限实例列表听起来好像你不需要一个类。

这当然本质上是编译器在你的类的场景中所做的事情。

这将允许您将实例列表作为函数导出到您的数据类型,并且您可以导出它们,但不能导出数据类型的构造函数。同样,您可以限制访问器函数的导出,只导出您真正想要的接口。

这很好用,因为数据类型不受类型类的模块边界交叉开放世界假设的影响。

有时增加类型系统的复杂性只会让事情变得更难。

于 2013-07-25T10:17:34.920 回答
6

如果您感兴趣的只是您有一组枚举实例,那么这个技巧可能会有所帮助:

class (Elem t '[Int, Integer, Bool] ~ True) => Closed t where

type family Elem (t :: k) (ts :: [k]) :: Bool where
  Elem a '[] = False
  Elem a (a ': as) = True
  Elem a (b ': bs) = Elem a bs

instance Closed Int
instance Closed Integer
instance Closed Bool
-- instance Closed Float -- ERROR
于 2015-04-13T11:26:17.960 回答
0

这是phipshabler 答案的另一个变体。这个不需要ConstraintKinds,应该避免需要UndecidableSuperClasses

type family Good a where
  Good Int = 'True
  Good Bool = 'True
  Good _ = 'False

class Good a ~ 'True => Closed a where ...
于 2022-01-03T16:17:44.210 回答