是否可以创建一个不能再接纳新成员的类型类(也许通过使用模块边界)?我可以拒绝导出完整实例定义所必需的函数,但这只会在有人生成无效实例时导致运行时错误。我可以让它成为编译时错误吗?
6 回答
我相信答案是肯定的,这取决于您要实现的目标。
您可以避免从接口模块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
没有人可以创建更多的类型实例PublicClass
2。
所有这一切都没有让您获得编译器将类视为“关闭”的能力。它仍然会抱怨可以通过选择“已关闭”的唯一可见实例来解决的模棱两可的类型变量。
1纯意见:通常最好有一个单独的内部模块,其名称很吓人,可以导出所有内容,以便您可以使用它进行测试/调试,并使用一个接口模块导入内部模块并仅导出您想要的东西出口。
2我猜想通过扩展有人可以声明一个新的重叠实例。例如,如果您提供了一个 for 的实例[a]
,那么有人可以声明一个新的PublicClass
for实例[Int]
,这将搭载SecretClass
for的实例[a]
。但鉴于它PublicClass
没有任何功能,而且他们无法编写一个实例,SecretClass
我看不出可以用它做多少事情。
您可以通过封闭类型族对封闭类型类进行编码,这些封闭类型族基本上可以依次编码为关联类型族。这个解决方案的关键是关联类型族的实例都在一个类型类实例中,并且每个单态类型只能有一个类型类实例。
请注意,这种方法独立于模块系统。我们不依赖于模块边界,而是提供了一个明确的列表,其中列出了哪些实例是合法的。这意味着,一方面,合法实例可以分布在多个模块甚至包中,另一方面,即使在同一个模块中,我们也不能提供非法实例。
对于这个答案,我假设我们要关闭以下类,以便它只能为类型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
是类型族的索引。c
for的族实例a
被编码为Instance c a
。由于c
也是类参数,c
因此必须在单个类实例声明中一起给出 的所有系列实例。
现在,我们使用这个框架来定义一个封闭的类型族MemberOfExample
来编码Int
和Integer
是Ok
,而所有其他类型都不是。
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)
您可以将类型类重构为数据声明(使用记录语法),其中包含您的类型类具有的所有功能。一个固定的有限实例列表听起来好像你不需要一个类。
这当然本质上是编译器在你的类的场景中所做的事情。
这将允许您将实例列表作为函数导出到您的数据类型,并且您可以导出它们,但不能导出数据类型的构造函数。同样,您可以限制访问器函数的导出,只导出您真正想要的接口。
这很好用,因为数据类型不受类型类的模块边界交叉开放世界假设的影响。
有时增加类型系统的复杂性只会让事情变得更难。
如果您感兴趣的只是您有一组枚举实例,那么这个技巧可能会有所帮助:
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
这是phipshabler 答案的另一个变体。这个不需要ConstraintKinds
,应该避免需要UndecidableSuperClasses
。
type family Good a where
Good Int = 'True
Good Bool = 'True
Good _ = 'False
class Good a ~ 'True => Closed a where ...