Haskell 中的孤立实例
当使用 -Wall
选项编译我的 Haskell 应用程序时,GHC 会抱怨孤立实例,例如:
Publisher.hs:45:9:
Warning: orphan instance: instance ToSElem Result
类型类 ToSElem
不是我的,它是由 HStringTemplate。
现在我知道如何解决这个问题(将实例声明移动到声明 Result 的模块中),并且我知道 为什么 GHC 更愿意避免孤立实例,但我仍然相信我的方法更好。我不在乎编译器是否带来不便——而不是它而不是我。
我想在 Publisher 模块中声明我的 ToSElem
实例的原因是,Publisher 模块依赖于 HStringTemplate,而不是其他模块。我试图保持关注点分离并避免每个模块都依赖于 HStringTemplate。
我认为,与 Java 的接口相比,Haskell 类型类的优点之一是它们是开放的而不是封闭的,因此不必在与数据类型相同的位置声明实例。 GHC 的建议似乎是忽略这一点。
因此,我正在寻找的要么是一些验证,证明我的想法是正确的,并且我有理由忽略/压制这个警告,要么是一个更有说服力的论据,反对按照我的方式做事。
When compiling my Haskell application with the -Wall
option, GHC complains about orphaned instances, for example:
Publisher.hs:45:9:
Warning: orphan instance: instance ToSElem Result
The type class ToSElem
is not mine, it's defined by HStringTemplate.
Now I know how to fix this (move the instance declaration into the module where Result is declared), and I know why GHC would prefer to avoid orphaned instances, but I still believe that my way is better. I don't care if the compiler is inconvenienced - rather it than me.
The reason I want to declare my ToSElem
instances in the Publisher module is because it is the Publisher module that depends on HStringTemplate, not the other modules. I am trying to maintain a separation of concerns and avoid having every module depend on HStringTemplate.
I thought that one of the advantages of Haskell's type classes, when compared for example to Java's interfaces, is that they are open rather than closed and therefore the instances do not have to be declared in the same place as the data type. GHC's advice seems to be to ignore this.
So, what I'm looking for is either some validation that my thinking is sound and that I would be justified in ignoring/suppressing this warning, or a more convincing argument against doing things my way.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
我理解你为什么要这样做,但不幸的是,Haskell 类似乎像你所说的那样“开放”,这可能只是一种错觉。许多人认为这样做的可能性是 Haskell 规范中的一个错误,原因我将在下面解释。无论如何,如果它确实不适合您需要在声明类的模块或声明类型的模块中声明的实例,这可能表明您应该使用 newtype 或您的类型的其他包装器。
需要避免孤立实例的原因远比编译器的便利性要深得多。这个话题颇有争议,你可以从其他答案中看到。为了平衡讨论,我将解释这样一种观点,即人们永远不应该编写孤儿实例,我认为这是经验丰富的 Haskellers 中的大多数观点。我自己的观点介于两者之间,我将在最后解释。
问题源于这样一个事实:当同一类和类型存在多个实例声明时,标准 Haskell 中没有机制来指定使用哪一个。相反,该程序会被编译器拒绝。
最简单的效果是,您可能拥有一个完美工作的程序,但由于其他人对您的模块的某些遥远依赖项所做的更改而突然停止编译。
更糟糕的是,工作程序可能会因为远程更改而在运行时崩溃。您可能正在使用一个假设来自某个实例声明的方法,并且它可能会默默地被另一个实例替换,该实例的不同足以导致您的程序开始莫名其妙地崩溃。
想要保证这些问题永远不会发生在他们身上的人必须遵循以下规则:如果任何人在任何地方曾经为某种类型声明过某个类的实例,则在编写的任何程序中都不得再次声明其他实例任何人。当然,有一种解决方法,即使用 newtype 来声明新实例,但这总是至少会带来一些小不便,有时甚至会带来很大的不便。
所以从这个意义上说,那些故意写孤儿实例的人是相当不礼貌的。
那么针对这个问题应该怎么办呢?反孤儿实例阵营表示,GHC 警告是一个错误,它必须是一个错误,拒绝任何声明孤儿实例的尝试。同时,我们必须自律,不惜一切代价避免它们。
正如您所看到的,有些人并不那么担心这些潜在的问题。正如您所建议的那样,他们实际上鼓励使用孤儿实例作为关注点分离的工具,并表示应该根据具体情况确保没有问题。我已经多次因为别人的孤儿事例而感到不便,因此确信这种态度过于漫不经心。
我认为正确的解决方案是为 Haskell 的导入机制添加一个扩展来控制实例的导入。这并不能完全解决问题,但它会为保护我们的程序免受世界上已经存在的孤儿实例的损害提供一些帮助。然后,随着时间的推移,我可能会相信,在某些有限的情况下,孤儿实例可能不会那么糟糕。 (正是这种诱惑就是反孤儿实例阵营中的一些人反对我的提议的原因。)
我从这一切中得出的结论是,至少在目前,我强烈建议您避免声明任何孤儿实例,如果没有其他原因的话,要为他人着想。使用
新类型
。I understand why you want to do this, but unfortunately, it may be only an illusion that Haskell classes seem to be "open" in the way that you say. Many people feel that the possibility of doing this is a bug in the Haskell specification, for reasons I'll explain below. Anyway, if it is really not appropriate for the instance you need to be declared either in the module where the class is declared or in the module where the type is declared, that is probably a sign that you should be using a
newtype
or some other wrapper around your type.The reasons why orphan instances need to be avoided run far deeper than convenience of the compiler. This topic is rather controversial, as you can see from other answers. To balance the discussion, I am going to explain the point of view that one should never, ever, write orphan instances, which I think is the majority opinion among experienced Haskellers. My own opinion is somewhere in the middle, which I'll explain at the end.
The problem stems from the fact that when more than one instance declaration exists for the same class and type, there is no mechanism in standard Haskell to specify which to use. Rather, the program is rejected by the compiler.
The simplest effect of that is that you could have a perfectly working program that would suddenly stop compiling because of a change someone else makes in some far off dependency of your module.
Even worse, it's possible for a working program to start crashing at runtime because of a distant change. You could be using a method that you are assuming comes from a certain instance declaration, and it could silently be replaced by a different instance that is just different enough to cause your program to start inexplicably crashing.
People who want guarantees that these problems won't ever happen to them must follow the rule that if anyone, anywhere, has ever declared an instance of a certain class for a certain type, no other instance must ever be declared again in any program written by anyone. Of course, there is the workaround of using a
newtype
to declare a new instance, but that is always at least a minor inconvenience, and sometimes a major one.So in this sense, those who write orphan instances intentionally are being rather impolite.
So what should be done about this problem? The anti-orphan-instance camp says that the GHC warning is a bug, it needs to be an error that rejects any attempt to declare an orphan instance. In the meantime, we must exercise self-discipline and avoid them at all costs.
As you have seen, there are those who are not so worried about those potential problems. They actually encourage the use of orphan instances as a tool for separation of concerns, as you suggest, and say that one should just make sure on a case-by-case basis that there is no problem. I have been inconvenienced enough times by other people's orphan instances to be convinced that this attitude is too cavalier.
I think the right solution would be to add an extension to Haskell's import mechanism that would control the import of instances. That would not solve the problems completely, but it would give some help towards protecting our programs against damage from the orphan instances that already exist in the world. And then, with time, I might become convinced that in certain limited cases, perhaps an orphan instance might not be so bad. (And that very temptation is the reason that some in the anti-orphan-instance camp are opposed to my proposal.)
My conclusion from all this is that at least for the time being, I would strongly advise that you avoid declaring any orphan instances, to be considerate to others if for no other reason. Use a
newtype
.继续压制这个警告吧!
你们有很好的伙伴。 Conal 在“TypeCompose”中完成此操作。 “chp-mtl”和“chp-transformers”执行此操作,“control-monad-exception-mtl”和“control-monad-exception-monadsfd”执行此操作,等等。
顺便说一句,您可能已经知道这一点,但对于那些不知道的人并在搜索中偶然发现您的问题:
编辑:
我承认 Yitz 在他的回答中提到的问题是真正的问题。然而,我认为不使用孤立实例也是一个问题,并且我尝试选择“万恶之最小”,恕我直言,谨慎使用孤立实例。
我在简短的回答中只使用了感叹号,因为你的问题表明你已经很清楚这些问题了。否则,我就不那么热情了:)
有点转移注意力,但我相信这是一个完美世界中的完美解决方案,无需妥协:
我相信Yitz提到的问题(不知道是哪个实例)被选择)可以在“整体”编程系统中解决,其中:
从幻想世界返回(或者希望是未来),现在:我建议尝试避免孤立实例,同时在“真正需要”时仍然使用它们
Go ahead and suppress this warning!
You are in good company. Conal does it in "TypeCompose". "chp-mtl" and "chp-transformers" do it, "control-monad-exception-mtl" and "control-monad-exception-monadsfd" do it, etc.
btw you probably already know this, but for those that don't and stumble your question on a search:
Edit:
I acknowledge the problems that Yitz mentioned in his answer as real problems. However I see not using orphaned instances as a problem as well, and I try to pick the "least of all evils", which is imho to prudently use orphan instances.
I only used an exclamation-point in my short answer because your question shows that you are already well aware of the problems. Otherwise, I would have been less enthusiastic :)
A bit of a diversion, but what I believe is the perfect solution in a perfect world without compromise:
I believe that the problems Yitz mentions (not knowing which instance is picked) could be solved in a "holistic" programming system where:
Back from fantasy world (or hopefully the future), right now: I recommend trying to avoid orphan instances while still using them when you "really need" to
孤儿实例很麻烦,但在我看来,它们有时是必要的。
我经常组合库,其中类型来自一个库,类来自另一个库。
当然,不能指望这些库的作者为每种可能的类型和类组合提供实例。所以我必须抚养他们,所以他们是孤儿。
当您需要提供实例时,应该将类型包装在新类型中的想法是一个具有理论价值的想法,但在许多情况下它太乏味了;这是那些不以编写 Haskell 代码为生的人提出的想法。 :)
所以继续提供孤立实例。它们是无害的。
如果您可以使用孤立实例使 ghc 崩溃,那么这是一个错误,应该如此报告。
( ghc 没有检测到多个实例的错误并不难修复。)
但请注意,将来某个时候其他人可能会添加您已经拥有的某个实例,并且您可能会得到(编译时)错误。
Orphan instances is a nuisance, but in my opinion they are sometimes necessary.
I often combine libraries where a type comes from one library and a class comes from another library.
Of course the authors of these libraries cannot be expected to provide instances for every conceivable combination of types and classes. So I have to provide them, and so they are orphans.
The idea that you should wrap the type in a new type when you need to provide an instance is an idea with theoretical merit, but it's just too tedious in many circumstances; it's the kind of idea put forward by people who don't write Haskell code for a living. :)
So go ahead and provide orphan instances. They are harmless.
If you can crash ghc with orphan instances then that is a bug and should be reported as such.
(The bug ghc had/has about not detecting multiple instances is not that hard to fix.)
But be aware that some time in the future someone else might add the some instance as you already have, and you might get a (compile time) error.
在这种情况下,我认为使用孤儿实例就可以了。对我来说,一般的经验法则是——如果您“拥有”类型类或者“拥有”数据类型(或其某些组件),则可以定义一个实例——即 Maybe MyData 的实例也可以,至少有时)。在这些限制内,您决定将实例放在哪里是您自己的事。
还有一个例外 - 如果您既不拥有类型类也不拥有数据类型,但生成的是二进制文件而不是库,那么也没关系。
In this case, I think the use of orphan instances is fine. The general rule of thumb for me is -- you can define an instance if you "own" the typeclass or if you "own" the data type (or some component thereof -- i.e., an instance for Maybe MyData is fine as well, at least sometimes). Within those constraints, where you decide to put the instance is your own business.
There's one further exception -- if you neither own the typeclass or the data type, but are producing a binary and not a library, then that's fine too.
(我知道我迟到了,但这可能对其他人仍然有用)
您可以将孤立实例保留在自己的模块中,然后如果有人导入该模块,那是因为他们需要它们,并且如果满足以下条件,他们可以避免导入它们:它们会造成问题。
(I know I'm late to the party but this may be still be useful to others)
You could keep the orphan instances in their own module, then if anyone imports that module it's specifically because they need them and they can avoid importing them if they cause problems.
沿着这些思路,我理解反孤儿实例阵营的立场 WRT 库,但对于可执行目标,孤儿实例不应该好吗?
Along these lines, I understand the anti-orphan instance camp's position WRT libraries, but for executable targets shouldn't orphan instances be fine?