或许通过一个思维实验可以稍微解释一下这种扩展。
假设我们放弃了多重模式定义函数必须都在同一个地方的限制,这样你就可以在模块的顶部写出 foo ("bar", Nothing) = ...
,然后在其他地方写出 foo ("baz", Just x) = ...
这样的模式。事实上,让我们更进一步,允许在完全不同的模块中定义模式!
如果你觉得这听起来会让使用变得混乱且容易出错,那么你是正确的。
为了恢复一些理智,我们可以添加一些限制。例如(哈哈),我们可以要求满足以下属性:
很明显,匹配简单构造函数如True
或Nothing
非常简单。我们还可以有点含糊其辞,假设编译器可以消除如上所述的字面量歧义,例如"bar"
和"baz"
。
然而,使用模式绑定参数如(x, Just y)
就变得非常棘手——写这样一个模式意味着放弃后续编写 (True, _)
或 (False, Just "foobar")
等模式的能力,因为那会产生歧义。更糟糕的是,模式保护几乎变得无用了,因为它们需要非常通用的匹配。许多常见的惯用语将导致无休止的歧义头痛,当然编写"默认"的落空模式也是完全不可能的。
这大致就是类型类实例的情况。
我们可以通过以下方式放宽所需属性来恢复一些表达能力:
请注意,我们现在处于这样一种情况:仅导入一个模块就可以通过引入一个新的、更具体的模式来改变函数的行为。在涉及高阶函数的复杂情况下,事情可能会变得混乱。尽管如此,在许多情况下,问题不太可能出现——例如,在库中定义一个通用的 fall-through 模式,同时让客户端代码添加必要的特定情况。
这大致就是 OverlappingInstances
所能提供的了。正如上面的示例所建议的那样,如果创建新的重叠总是不可能或者是期望的,并且不同的模块不会看到不同的、相互冲突的实例,那么它可能是没问题的。
真正关键的是,OverlappingInstances
移除的限制是为了使在“开放世界”假设下使用类型类的行为合理化,即任何可能的实例以后都可以添加。通过放松这些要求,您将自行承担这个负担;因此,请仔细考虑新实例的添加方式以及这些情况是否会带来重大问题。如果您确信即使在偏僻和狡猾的边角案例中也不会出现任何问题,那么就继续使用该扩展。
大多数人请求重叠实例是因为他们想要约束导向推理而不是类型导向推理。类型类是为了类型导向推理而设计的,Haskell没有提供优雅的解决方案来进行约束导向推理。
然而,您仍然可以通过使用newtype来“封装好处”。 给定以下容易出现重叠实例的实例定义:
instance (SomeConstraint a) => SomeClass a where ...
您可以使用以下代码替代:
newtype N a = N { unN :: a }
instance (SomeConstraint a) => SomeClass (N a) where ...
现在Haskell的类型类系统有了一个专门的类型来匹配,即N a
,而不是随意匹配每一种类型。这使得您可以控制实例的范围,因为只有被N
newtype包装的内容才能匹配。
newtype
来包装它以获得所需的实例。 - crockeeaOverlappingInstances
可以让你在类型类级别上编写许多有用的代码,否则无法实现。然而,其中绝大部分可以重组以使用单个函数依赖(以此处的种类多态风格编写)。
class TypeEq (a :: k) (b :: k) (t :: Bool) | a b -> t where
typeEq :: Proxy a -> Proxy b -> HBool t
目前只能使用OverlappingInstance
(以完全通用的方式)来实现此功能。使用案例包括Oleg将OOP编码为Haskell。因此,我认为OverlappingInstances
的一个好用例是经典HList论文中TypeEq的这个实现。
这种特定功能可以非常轻松地通过编译器支持提供(甚至可以在类型函数而不是fundep级别上工作),因此在某个地方粘贴一个带有TypeEq的单个模块对我来说并不那么糟糕。
当我从事危险的类型类黑客工作时,我经常发现IncoherentInstances
行为(选择第一个匹配的实例)更易于推理和更灵活,因此至少在设计的探索阶段使用它。一旦我有了想要的东西,我会尝试去除扩展,特别注意那些行为不良的扩展(像这些)。
TypeEq
可以从非重叠实例中使用,因此确实可以隔离扩展的使用。您不能将其与类型族一起使用,但这是一个不同的问题。顺便说一下,IncoherentInstances
大致意味着放弃我答案中“宽松”属性的第三个。 GHC 不再要求实例选择与实际使用一致,并在出现歧义时抱怨,而是会选择看起来最好的实例。耶! - C. A. McCann