Orphaned instances in Haskell
Asked Answered
B

6

88

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.

Blackford answered 20/6, 2010 at 14:17 Comment(1)
The discussion in the answers and comments illustrates that there's a big difference between defining orphan instances in an executable, as you're doing, vs. in a library that's exposed to others. This tremendously popular question illustrates how confusing orphan instances can be for end-users of a library that defines them.Oxalate
F
98

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.

Fimble answered 20/6, 2010 at 15:22 Comment(5)
In particular, this is increasingly a problem with the growth in libraries. With > 2200 libraries on Haskell, and 10s of thousands of individual modules, the risk of picking up instances grows dramatially.Svetlanasvoboda
Re: "I think the right solution would be to add an extension to Haskell's import mechanism that would control the import of instances" In case this idea interests anyone, it might be worth looking at the Scala language for an example; it has features very much like this for controlling the scope of 'implicits', which can be used very much like typeclass instances.Excommunicate
My software is an application rather than a library, so the possibility of causing problems for other developers is pretty much zero. You could consider the Publisher module the application and the rest of the modules as a library but if I were to distribute the library it would be without the Publisher and, therefore, the orphaned instances. But if I moved the instances into the other modules, the library would ship with an unnecessary dependency on HStringTemplate. So in this case I think the orphans are OK, but I will heed your advice if I encounter the same problem in a different context.Blackford
That sounds like a reasonable approach. The only thing to watch out for then is if the author of a module you import adds this instance in a later version. If that instance is the same as yours, you'll need to delete your own instance declaration. If that instance is different than yours, you'll need to put a newtype wrapper around your type - which could be a significant refactoring of your code.Fimble
@Matt: indeed, surprisingly Scala gets this one right where Haskell doesn't! (except of course Scala lacks first class syntax for type class machinery, which is even worse...)Castalia
E
47

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:

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

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:

  • You are not editing mere text files primitively, but are rather assisted by the environment (for example code completion only suggest things of relevant types etc)
  • The "lower level" language has no special support for type-classes, and instead function tables are passed along explicitly
  • But, the "higher level" programming environment displays the code in similar way to how Haskell is presented now (you usually won't see the function tables passed along), and picks the explicit type-classes for you when they are obvious (for example all cases of Functor have only one choice) and when there are several examples (zipping list Applicative or list-monad Applicative, First/Last/lift maybe Monoid) it lets you choose which instance to use.
  • In any case, even when the instance was picked for you automatically, the environment easily allows you to see which instance was used, with an easy interface (a hyperlink or hover interface or something)

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

Endocarp answered 20/6, 2010 at 14:31 Comment(2)
Yes, but arguably each of those occurrences is a mistake of some order. The bad instances in control-monad-exception-mtl and monads-fd for Either come to mind. It would be less obtrusive were each of those modules to be forced to define their own types or supply newtype wrappers. Almost every orphan instance is a headache waiting to happen, and if nothing else will require your constant vigilance to ensure that it is imported or not as appropriate.Weitzman
Thanks. I think I will use them in this particular situation, but thanks to Yitz I now have a better appreciation of what problems they can cause.Blackford
D
39

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.

Damp answered 21/6, 2010 at 1:53 Comment(1)
A good example is (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v) when using QuickCheck.Castalia
W
18

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.

Wire answered 20/6, 2010 at 23:43 Comment(0)
D
6

(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.

Deficient answered 17/2, 2011 at 1:33 Comment(0)
G
4

Along these lines, I understand the anti-orphan instance camp's position WRT libraries, but for executable targets shouldn't orphan instances be fine?

Gravel answered 22/6, 2010 at 10:58 Comment(2)
In terms of being impolite to others, you are right. But you are opening yourself to potential future problems if the same instance is ever defined in the future somewhere in your dependency chain. So in this case, it's up to you to decide whether it's worth the risk.Fimble
In almost all cases of implementing an orphan instance in an executable, it's to fill a gap you wish was already defined for you. So if the instance appears upstream, the resulting compile error is a just a useful signal to tell you you can remove your declaration of the instance.Veach

© 2022 - 2024 — McMap. All rights reserved.