88

当使用该选项编译我的 Haskell 应用程序时-Wall,GHC 抱怨孤立实例,例如:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

类型类ToSElem不是我的,它是由HStringTemplate定义的。

现在我知道如何解决这个问题(将实例声明移动到声明 Result 的模块中),并且我知道为什么 GHC 更愿意避免孤立实例,但我仍然相信我的方式更好。我不在乎编译器是否给您带来不便——而不是我。

我想在 Publisher 模块中声明我的ToSElem实例的原因是因为它是 Publisher 模块依赖于 HStringTemplate,而不是其他模块。我试图保持关注点分离并避免每个模块都依赖于 HStringTemplate。

我认为 Haskell 类型类的优点之一是,例如与 Java 的接口相比,它们是开放的而不是封闭的,因此不必在与数据类型相同的位置声明实例。GHC 的建议似乎是忽略这一点。

所以,我正在寻找的是一些验证,证明我的想法是正确的,并且我有理由忽略/抑制这个警告,或者是一个更有说服力的反对以我的方式做事的论据。

4

6 回答 6

98

我理解您为什么要这样做,但不幸的是,Haskell 类似乎以您所说的方式“开放”可能只是一种错觉。许多人认为这样做的可能性是 Haskell 规范中的一个错误,原因我将在下面解释。无论如何,如果它确实不适合您需要在声明类的模块或声明类型的模块中声明的实例,这可能表明您应该使用一个newtype或其他包装器围绕你的类型。

需要避免孤儿实例的原因远不止编译器的便利性。从其他答案中可以看出,这个话题颇具争议。为了平衡讨论,我将解释一个观点,即永远不应该编写孤儿实例,我认为这是有经验的 Haskellers 中的大多数观点。我自己的观点介于两者之间,我将在最后解释。

问题源于这样一个事实,即当同一类和类型存在多个实例声明时,标准 Haskell 中没有指定使用哪一个的机制。相反,该程序被编译器拒绝。

最简单的效果是,您可能拥有一个完美运行的程序,但由于其他人对您的模块的某个遥远的依赖项所做的更改而突然停止编译。

更糟糕的是,一个正在运行的程序有可能因为一个遥远的变化而在运行时开始崩溃。您可能正在使用您假设来自某个实例声明的方法,并且它可以被一个不同的实例悄悄地替换,该实例的差异足以导致您的程序开始莫名其妙地崩溃。

想要保证这些问题永远不会发生在他们身上的人必须遵循这样的规则:如果任何人在任何地方曾经为某种类型声明过某个类的实例,那么在任何编写的程序中都不能再声明其他实例由任何人。当然,也有使用 anewtype来声明一个新实例的解决方法,但这总是至少是一个小的不便,有时是一个主要的不便。所以从这个意义上说,那些故意写孤儿实例的人是相当不礼貌的。

那么这个问题应该怎么办呢?反孤儿实例阵营说 GHC 警告是一个错误,它需要是一个错误,拒绝任何声明孤儿实例的尝试。与此同时,我们必须自律,不惜一切代价避免它们。

如您所见,有些人并不那么担心那些潜在的问题。正如您所建议的,他们实际上鼓励使用孤立实例作为分离关注点的工具,并说应该根据具体情况确保没有问题。其他人的孤儿实例给我带来了足够多的不便,以至于我确信这种态度太漫不经心了。

我认为正确的解决方案是为 Haskell 的导入机制添加一个扩展,以控制实例的导入。这并不能完全解决问题,但它有助于保护我们的程序免受世界上已经存在的孤儿实例的损害。然后,随着时间的推移,我可能会相信在某些有限的情况下,孤儿实例可能不会那么糟糕。(而正是这个诱惑,就是反孤例阵营中的一些人反对我的提议的原因。)

我从这一切中得出的结论是,至少在目前,我强烈建议您避免声明任何孤立实例,如果没有其他原因,请为他人着想。使用newtype.

于 2010-06-20T15:22:01.103 回答
45

继续压制这个警告!

你有很好的陪伴。Conal 在“TypeCompose”中执行此操作。“chp-mtl”和“chp-transformers”执行,“control-monad-exception-mtl”和“control-monad-exception-monadsfd”执行,等等。

顺便说一句,您可能已经知道这一点,但是对于那些不知道并且在搜索中偶然发现您的问题的人:

{-# OPTIONS_GHC -fno-warn-orphans #-}

编辑:

我承认 Yitz 在他的回答中提到的问题是真正的问题。但是,我认为不使用孤立实例也是一个问题,并且我尝试选择“万恶之源”,恕我直言,谨慎使用孤立实例。

我在简短的回答中只使用了感叹号,因为您的问题表明您已经很清楚这些问题。否则,我会不那么热情:)

有点分心,但我相信这是完美世界中不妥协的完美解决方案:

我相信 Yitz 提到的问题(不知道选择了哪个实例)可以在“整体”编程系统中解决,其中:

  • 您不是在原始地编辑文本文件,而是在环境的帮助下(例如代码完成仅建议相关类型的内容等)
  • “低级”语言对类型类没有特殊支持,而是显式传递函数表
  • 但是,“更高级别”的编程环境以与 Haskell 现在的呈现方式类似的方式显示代码(您通常不会看到传递的函数表),并在它们明显时为您选择显式类型类(例如例如 Functor 的所有情况只有一个选择)并且当有多个示例(压缩列表 Applicative 或 list-monad Applicative,First/Last/lift 可能是 Monoid)时,它可以让您选择要使用的实例。
  • 在任何情况下,即使自动为您选择了实例,环境也可以通过简单的界面(超链接或悬停界面等)轻松地让您查看使用了哪个实例

从幻想世界(或希望未来)回来,现在:我建议尽量避免孤儿实例,同时在“真正需要”时仍然使用它们

于 2010-06-20T14:31:29.137 回答
37

孤儿实例很麻烦,但在我看来,它们有时是必要的。我经常组合库,其中一个类型来自一个库,一个类来自另一个库。当然,不能期望这些库的作者为类型和类的每种可能的组合提供实例。所以我必须提供他们,所以他们是孤儿。

当你需要提供一个实例时你应该将类型包装在一个新类型中的想法是一个具有理论价值的想法,但在许多情况下它太乏味了。这是那些不以编写 Haskell 代码为生的人提出的想法。:)

所以继续提供孤儿实例。它们是无害的。
如果您可以使用孤立实例使 ghc 崩溃,那么这是一个错误,应该这样报告。(ghc 有/有关于不检测多个实例的错误并不难修复。)

但是请注意,将来某个时候其他人可能会添加您已经拥有的某些实例,并且您可能会收到(编译时)错误。

于 2010-06-21T01:53:26.263 回答
18

在这种情况下,我认为使用孤儿实例很好。对我来说,一般的经验法则是——如果你“拥有”类型类或者如果你“拥有”数据类型(或其某些组件——即,Maybe MyData 的实例也可以,则可以定义一个实例,至少有时)。在这些限制条件下,您决定将实例放在哪里是您自己的事。

还有一个例外——如果你既不拥有类型类也不拥有数据类型,但生成的是二进制文件而不是库,那也没关系。

于 2010-06-20T23:43:58.963 回答
5

(我知道我迟到了,但这可能对其他人仍然有用)

您可以将孤儿实例保留在他们自己的模块中,然后如果有人导入该模块,那是因为他们需要它们,并且如果它们引起问题,他们可以避免导入它们。

于 2011-02-17T01:33:19.273 回答
4

沿着这些思路,我了解反孤儿实例阵营的立场 WRT 库,但对于可执行目标,孤儿实例不应该没问题吗?

于 2010-06-22T10:58:48.433 回答