17

Pivoting off some recent questions, I figured I'd turn the spotlight on the old bogeyman, OverlappingInstances.

A few years ago I might've been asking this question in earnest: after all, you can provide useful default instances and others can override them with more specific ones when they need to, what can be so bad about that?

Along the way I've absorbed some apprecation for the viewpoint that OverlappingInstances is really not so clean, and best avoided; mainly stemming from the fact that it's not very well-grounded theoretically, unlike other big extensions.

But thinking about it, I'm not sure if I could explain what's really so bad about it to another person, if I were asked.

What I'm looking for is specific examples of ways in which using OverlappingInstances can lead to bad things happening, whether it's by subverting the type system or other invariants, or just general unexpectedness or messiness.

One particular problem I know of is that it breaks the property that merely adding or removing a single module import can't change the meaning of your program, because with the extension on, a new instance overlap could be silently added or removed. While I can see why that's unpleasant, I don't see why it's earth-shatteringly awful.

Bonus question: As long as we're on the subject of useful but not theoretically well-grounded extensions that can lead to bad happenings, how come GeneralizedNewtypeDeriving doesn't get the same bad rap? Is it because the negative possibilities are more easy to localize; that it's easier to see what would cause problems and say, "don't do that"?

(Note: I would prefer if the brunt of the answer focuses on OverlappingInstances, not IncoherentInstances which needs less explanation.)

EDIT: There are also good answers to a similar question here.

Community
  • 1
  • 1
glaebhoerl
  • 7,695
  • 3
  • 30
  • 41
  • 6
    The badness about `GeneralizedNewtypeDeriving` is an implementation bug. There's nothing bad about this extension as such, but ghc allows it in cases where it should be banned. – augustss Jun 08 '12 at 02:45
  • 1
    Related: http://stackoverflow.com/questions/10830757/is-there-a-list-of-ghc-extensions-that-are-considered-safe – Lambda Fairy Jun 08 '12 at 04:21
  • 2
    @augustss, is it that simple? See http://hackage.haskell.org/trac/ghc/ticket/5498. Simon says he doesn't know of a simple syntactic test to determine when it should be banned, and that it requires new theoretical work and advancements in the type checker to make it safe. – glaebhoerl Jun 08 '12 at 14:28
  • 2
    @illissius I didn't say it was an easy bug to fix, but still just an implementation bug. If `GeneralizedNewtypeDeriving` constructed real instances instead of playing tricks with type coercions this bug would not occur (which is how I implemented it 10 years ago in Bluespec). – augustss Jun 09 '12 at 05:09
  • Ah, I see. I think that's exactly the part of it I was thinking of as not well grounded. Different perspectives. – glaebhoerl Jun 09 '12 at 10:01

1 Answers1

19

One principle that the haskell language attempts to abide by is adding extra methods/classes or instances in a given module should not cause any other modules that depend on the given module to either fail to compile or have different behaviour (as long as the dependent modules use explicit import lists).

Unfortunately, this is broken with OverlappingInstances. For example:

Module 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

Module B:

module B where
import A (Test(test))

someFunc :: String -> Int -> String
someFunc = test

shouldEqualHello = someFunc "hello" 4

shouldEqualHello does equal "hello" in module B.

Now add the following instance declaration in A:

instance Test String Int String where
    test s i = concat $ replicate i s

It would be preferable if this didn't affect module B. It worked before this addition, and should work afterwards. Unfortunately, this isn't the case.

Module B still compiles, but now shouldEqualHello now equals "hellohellohellohello". The behaviour has changed even though no method it was originally using had changed.

What is worse is there is no way to go back to the old behaviour, as you cannot choose to not import an instance from a module. As you can imagine, this is very bad for backwards compatibility, as you cannot safely add new instances to a class that uses overlappinginstances, as it could change the behaviour of code that uses the module (especially true if you are writing library code). This is worse than a compile error, as it could be very difficult to track down the change.

The only safe time to use overlapping instances in my opinion is when you are writing a class that you know will never need additional instances. This may occur if you are doing some tricky type based code.

David Miani
  • 14,518
  • 2
  • 47
  • 66
  • 1
    I see. That's basically the issue I acknowledged in the question, but you've done a good job of explaining why it's bad. Is this the only real problem with it? – glaebhoerl Jun 08 '12 at 14:07
  • 6
    Yes, the 'only' real problem is that you can completely lose confluence and the ability to reason about your code. It kind of works if you are careful to define all instances that overlap in one module, but there are ways to abuse constraint kinds to do bad things where you pass the more specific case the less specific instance. – Edward Kmett Jun 12 '12 at 00:07
  • 1
    @EdwardKmett What sense are you using the word "confluence" in here? The same one as for term rewriting systems? – glaebhoerl Feb 10 '14 at 07:54
  • 1
    Yes. Though, coherence is also used. – Edward Kmett Mar 27 '14 at 03:48
  • 1
    @EdwardKMETT Can you elaborate on the constraint kind stuff? I assumed that as long as you don't define orphans or do any incoherent instance stuff that you were pretty safe with overlapping instances. – semicolon Feb 14 '17 at 04:40
  • Yes, the change in behaviour is as @David Miani describes. Why is this bad? Why is it anything to do with OverlappingInstances? The developer of Module A could make any change to method/class test/Test; not necessarily adding an instance. Presumably it's an improvement, so of course the importer wants the improved behaviour! – AntC Apr 17 '17 at 02:02