17

围绕最近的一些问题,我想我会把焦点转向老妖怪,OverlappingInstances.

几年前我可能一直在认真地问这个问题:毕竟,您可以提供有用的默认实例,而其他人可以在需要时用更具体​​的实例覆盖它们,这有什么不好呢?

一路走来,我对OverlappingInstances确实不那么干净,最好避免的观点产生了一些欣赏;主要是因为它在理论上的基础不是很好,不像其他大的扩展。

但仔细想想,如果有人问我,我不确定我是否可以向另一个人解释它到底有什么不好。

我正在寻找的是使用OverlappingInstances可能导致坏事发生的具体示例,无论是通过颠覆类型系统或其他不变量,还是只是一般的意外或混乱。

我知道的一个特殊问题是它破坏了仅添加或删除单个模块导入不能改变程序含义的属性,因为随着扩展的打开,可以静默添加或删除新的实例重叠。虽然我可以理解为什么这令人不快,但我不明白为什么它会如此可怕。

额外的问题:只要我们讨论的是有用但理论上没有充分基础的扩展,这些扩展可能会导致糟糕的事情发生,为什么GeneralizedNewtypeDeriving没有得到同样糟糕的说唱?是不是因为负面的可能性更容易定位?更容易看出什么会导致问题并说“不要那样做”?

(注意:如果答案的首当其冲集中在 上,我更愿意OverlappingInstances,而不是IncoherentInstances需要较少的解释。)

编辑:这里也有类似问题的好答案。

4

1 回答 1

19

haskell 语言试图遵守的一个原则是在给定模块中添加额外的方法/类或实例不应导致任何其他依赖于给定模块的模块无法编译或具有不同的行为(只要依赖的模块使用显式导入列表)。

不幸的是,这被 OverlappingInstances 打破了。例如:

模块 A:

{-# LANGUAGE FlexibleInstances, OverlappingInstances, MultiParamTypeClasses, FunctionalDependencies #-}

module A (Test(..)) where

class Test a b c | a b -> c where
   test :: a -> b -> c

instance Test String a String where
    test str _ = str

模块 B:

module B where
import A (Test(test))

someFunc :: String -> Int -> String
someFunc = test

shouldEqualHello = someFunc "hello" 4

shouldEqualHello确实等于模块 B 中的“hello”。

现在在 A 中添加以下实例声明:

instance Test String Int String where
    test s i = concat $ replicate i s

如果这不影响模块 B 会更好。它在添加之前工作,并且应该在之后工作。不幸的是,事实并非如此。

模块 B 仍然可以编译,但shouldEqualHello现在等于"hellohellohellohello"。即使最初使用的方法没有改变,行为也发生了变化。

更糟糕的是没有办法回到旧的行为,因为你不能选择不从模块中导入实例。可以想象,这对向后兼容性非常不利,因为您无法安全地将新实例添加到使用重叠实例的类中,因为它可能会改变使用模块的代码的行为(尤其是在您编写库代码时)。这比编译错误更糟糕,因为跟踪更改可能非常困难。

在我看来,使用重叠实例的唯一安全时间是当你编写一个你知道永远不需要额外实例的类时。如果您正在执行一些基于类型的棘手代码,则可能会发生这种情况。

于 2012-06-08T04:13:58.560 回答