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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With