Haskell中的孤立实例


86

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

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

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

现在,我知道如何解决此问题(将实例声明移动到声明Result的模块中),并且我知道为什么GHC宁愿避免使用孤立的实例,但我仍然相信我的方法会更好。我不在乎编译器是否不便-而不是我。

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

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

因此,我正在寻找的要么是验证我的想法是正确的,要么是忽略/抑制该警告是有道理的,要么是更令人信服的反对以我的方式做事的论据。


答案和评论中的讨论表明,在您执行操作时,在可执行文件中定义孤立实例与在暴露给他人的中定义孤立实例之间存在很大差异。这个非常受欢迎的问题说明了对于定义它们的库的最终用户而言,令人困惑的孤立实例是多么的令人困惑。
Christian Conkle 2014年

Answers:


94

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

需要避免孤立实例的原因远远超出了编译器的便利性。从其他答案中可以看出,该主题颇具争议。为了平衡讨论,我将解释一种观点,即永远不要写孤立的实例,我认为这是经验丰富的Haskellers中的多数意见。我的观点在中间,我将在最后解释。

问题源于以下事实:当同一类和类型存在多个实例声明时,标准Haskell中没有机制来指定要使用的实例。而是,程序被编译器拒绝。

最简单的效果是,您可以拥有一个运行良好的程序,由于其他人对您的模块的依赖性很大,因此该程序会突然停止编译。

更糟糕的是,由于远距离的更改,正在运行的程序可能会在运行时开始崩溃。您可能使用的是假设来自某个实例声明的方法,并且可以用另一个实例替代它,而该实例恰好足以导致您的程序莫名其妙地崩溃,因此它可能会被无声地替换。

想要保证这些问题永远不会发生的人必须遵循以下规则:如果任何人在任何地方都已经为某种类型声明了某个类的实例,则在编写的任何程序中都不得再次声明其他实例任何人。当然,有一种使用anewtype声明新实例的解决方法,但这始终至少是一个小麻烦,有时是一个大麻烦。因此,从这个意义上讲,那些故意编写孤立实例的人是相当不礼貌的。

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

如您所见,有些人并不那么担心那些潜在的问题。正如您所建议的那样,他们实际上鼓励使用孤立实例作为关注点分离的工具,并说应该仅在逐案的基础上确保没有问题。别人的孤儿给我带来了很多不便,使我确信这种态度过于武断。

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

从所有这一切得出的结论是,至少暂时而言,我强烈建议您避免声明任何孤立实例,如果没有其他原因,则应考虑其他实例。使用newtype


4
尤其是,随着库的增长,这越来越成为一个问题。Haskell上有超过2200个库,并且有成千上万个单独的模块,因此拾取实例的风险急剧增加。
唐·斯图尔特

16
回复:“我认为正确的解决方案是对Haskell的导入机制进行扩展以控制实例的导入”。它具有非常类似的功能来控制“隐式”的范围,可以像类型类实例一样使用。
马特2010年

5
我的软件是应用程序,而不是库,因此对其他开发人员造成问题的可能性几乎为零。您可以将发布者模块视为应用程序,并将其余模块视为一个库,但是如果我要分发该库,它将没有发布者,因此也没有孤立的实例。但是,如果我将实例移到其他模块中,则该库将附带对HStringTemplate的不必要的依赖关系。因此,在这种情况下,我认为这些孤儿都可以,但是如果我在不同的情况下遇到相同的问题,我会听取您的建议。
Dan Dyer 2010年

1
这听起来很合理。唯一需要注意的是,如果您导入的模块的作者在更高版本中添加了该实例。如果该实例与您的实例相同,则需要删除自己的实例声明。如果该实例与您的实例不同,则需要在类型周围放置一个新类型包装器-这可能是代码的重要重构。
Yitz 2010年

@Matt:确实,令人惊讶的是Scala做到了Haskell没有做到的这一点!(当然,Scala缺乏类型类机器的一流语法,这甚至更糟……)
Erik Kaplun 2015年

44

继续抑制此警告!

你相处得很好。圆锥体在“ TypeCompose”中完成。用“ chp-mtl”和“ chp-transformers”来做,“ control-monad-exception-mtl”和“ control-monad-exception-monadsfd”等等。

顺便说一句,您可能已经知道这一点,但是对于那些不了解并在搜索中绊倒您的问题的人:

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

编辑:

我承认耶茨在回答中提到的问题是真正的问题。但是,我也认为不要将孤立的实例用作问题,因此,我尝试选择“万无一失”,谨慎地使用孤立的实例是不可能的。

我只在简短回答中使用了一个感叹号,因为您的问题表明您已经充分意识到了这些问题。否则,我本来就不会那么热情:)

有点转移,但我认为是完美世界中不妥协的完美解决方案:

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

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

现在回到幻想世界(或者希望是未来):我建议您在真正需要时尝试避免使用孤立实例,同时仍在使用它们


5
是的,但是可以说这些事件中的每一个都是某种顺序的错误。我想起了control-monad-exception-mtl和monads-fd中对于Either的不良实例。如果每个模块都被迫定义自己的类型或提供新型包装器,那么将不会那么麻烦。几乎每个孤儿实例都令人头疼,等待发生,如果没有其他事情需要您持续保持警惕,以确保将其导入或不适当导入。
爱德华KMETT

2
谢谢。我想我将在特定情况下使用它们,但是由于有了Yitz,我现在对它们可能引起的问题有了更好的理解。
Dan Dyer 2010年

37

孤儿实例是一件令人讨厌的事,但我认为有时它们是必要的。我经常组合库,其中类型来自一个库,而类来自另一个库。当然,不能期望这些库的作者为每种可能的类型和类组合提供实例。所以我必须提供他们,所以他们是孤儿。

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

因此,继续并提供孤立实例。他们是无害的。
如果您可以将ghc与孤立实例一起崩溃,则这是一个错误,应这样报告。(ghc关于/不检测多个实例的错误并不难解决。)

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


2
(Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)使用QuickCheck就是一个很好的例子。
埃里克·卡普伦

17

在这种情况下,我认为使用孤立实例很好。对我而言,一般的经验法则是-如果您“拥有”类型类或“拥有”数据类型(或其某些组件),则可以定义一个实例-即,也许MyData的实例也很好,至少有时)。在这些限制条件下,您决定将实例放置在自己的公司中。

还有一个例外-如果您既不拥有typeclass也不是数据类型,但是生成的是二进制文件而不是库,那也很好。


5

(我知道我迟到聚会了,但这可能对其他人还是有用的)

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


3

沿着这些思路,我了解了反孤儿实例阵营的WRT库的位置,但是对于可执行目标,孤儿实例不应该很好吗?


3
在不礼貌的方面,你是对的。但是,如果将来在依赖关系链中的某个位置定义了相同的实例,那么您将面临潜在的未来问题。因此,在这种情况下,由您决定是否值得冒险。
Yitz 2010年

5
在几乎所有在可执行文件中实现孤立实例的情况下,都是为了填补您希望已经为您定义的空白。因此,如果实例出现在上游,则产生的编译错误只是一个有用的信号,告诉您可以删除实例的声明。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.