透过最近的一些问题,我想我会把焦点放在旧的怪物身上,OverlappingInstances
。
几年前,我可能会认真地问这个问题:毕竟,你可以提供有用的默认实例,而其他人可以在需要时用更具体的实例覆盖它们,那可能是多么糟糕?< / p>
在此过程中,我对OverlappingInstances
真的不那么干净,最好避免的观点表示赞赏;主要源于这样一个事实,即理论上它不是很有根据,不像其他大的扩展。
但是考虑到这一点,如果有人问我,我不确定是否可以向另一个人解释它真的如此糟糕。
我正在寻找的是使用OverlappingInstances
导致不良事件发生的方式的具体示例,无论是通过颠覆类型系统还是其他不变量,还是只是一般意外或混乱。
我所知道的一个特殊问题是它破坏了仅添加或删除单个模块导入的属性不能改变程序含义的属性,因为扩展打开时,可以静默添加或删除新的实例重叠。虽然我可以看到为什么这是令人不快的,但我不明白为什么它是惊天动地的糟糕。
奖金问题:只要我们谈论有用但非理论上有根据的扩展可能导致不良事件,那么GeneralizedNewtypeDeriving
怎么会得到同样糟糕的说唱?是因为负面可能性更容易本地化;更容易看出会导致问题的原因并说“不要那样做”?
(注意:如果答案主要集中在OverlappingInstances
,而不是IncoherentInstances
需要较少解释,我更愿意。)
编辑:类似问题here也有很好的答案。
答案 0 :(得分:18)
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中等于“你好”。
现在在A:
中添加以下实例声明instance Test String Int String where
test s i = concat $ replicate i s
如果这不影响模块B,那将是更好的。它在此添加之前有效,并且应该在之后工作。不幸的是,事实并非如此。
模块B仍在编译,但现在shouldEqualHello
现在等于"hellohellohellohello"
。即使最初使用的方法没有更改,行为也已更改。
更糟糕的是,无法回到旧行为,因为您无法选择不从模块导入实例。您可以想象,这对于向后兼容性非常糟糕,因为您无法安全地将新实例添加到使用重叠实例的类中,因为它可能会更改使用该模块的代码的行为(尤其是在编写库代码时)。这比编译错误更糟糕,因为跟踪更改可能非常困难。
在我看来,使用重叠实例的唯一安全时间是在编写一个您知道永远不需要其他实例的类时。如果你正在做一些棘手的基于类型的代码,可能会发生这种情况。