13

Why does this result in a conflict?

class Foo a b | b -> a where
  foo :: a -> b -> Bool

instance Eq a => Foo a a where
  foo = (==)

instance Eq a => Foo a (a -> a) where
  foo x f = f x == x

Note that the code will compile if I remove the functional dependecy.

I was under the impression that functional dependencies should only disallow stuff like the following, when in fact, it compiles!

class Foo a b | b -> a where
  foo :: a -> b -> Bool

instance Eq a => Foo a a where
  foo = (==)

instance Eq a => Foo Bool a where
  foo _ x = x == x

Same b parameter, yet different a parameters. Shouldn't b -> a disallow this, as this means a is uniquely determined by b?

Thomas Eding
  • 35,312
  • 13
  • 75
  • 106

2 Answers2

15

Have you tried actually using the second version? I'm guessing that while the instances compile, you'll start getting ambiguity and overlap errors when you call foo.

The biggest stumbling block here is that fundeps don't interact with type variables the way you might expect them to--instance selection doesn't really look for solutions, it just blindly matches by attempting unification. Specifically, when you write Foo a a, the a is completely arbitrary, and can thus unify with a type like b -> b. When the second parameter has the form b -> b, it therefore matches both instances, but the fundeps say that in one case the first parameter should be b -> b, but in the other that it should be b. Hence the conflict.


Since this apparently surprises people, here's what happens if you try to use the second version:

  • bar = foo () () results in:

    Couldn't match type `Bool' with `()'
      When using functional dependencies to combine
        Foo Bool a,
    

    ...because the fundep says, via the second instance, that any type as the second parameter uniquely determines Bool as the first. So the first parameter must be Bool.

  • bar = foo True () results in:

    Couldn't match type `()' with `Bool'
      When using functional dependencies to combine
        Foo a a,
    

    ...because the fundep says, via the first instance, that any type as the second parameter uniquely determines the same type for the first. So the first parameter must be ().

  • bar = foo () True results in errors due to both instances, since this time they agree that the first parameter should be Bool.

  • bar = foo True True results in:

    Overlapping instances for Foo Bool Bool
      arising from a use of `foo'
    

    ...because both instances are satisfied, and therefore overlap.

Pretty fun, huh?

C. A. McCann
  • 76,893
  • 19
  • 209
  • 302
  • weirdly enough, the second version *does* work, although I can't imagine why! – sclv Sep 15 '11 at 19:37
  • But, if the instances compile, what's the point of functional dependencies? I thought they were exactly to prevent this kind of thing from compiling! – Daniel Wagner Sep 15 '11 at 19:40
  • 4
    @Daniel Wagner: Writing instances that can never work, but are accepted by the compiler as long as you don't try to use them, is a known flaw of fundeps to the best of my knowledge. It's one of the reasons why I prefer type families for most purposes. – C. A. McCann Sep 15 '11 at 19:44
2

The first instance says for any a then the fundep gives you back an a. This means that it will exclude pretty much anything else, since anything else should unify with that free variable and hence force the choice of that instance.

Edit: Initially I had suggested the second example worked. It did on ghc 7.0.4, but it didn't make sense that it did so, and this issue seems to have been resolved in newer versions.

sclv
  • 38,665
  • 7
  • 99
  • 204
  • The second example doesn't work. It's just GHC being indecisive about instances due to polymorphism. If you give it something more explicit, or a concrete type to find an instance for, it yells at you. – C. A. McCann Sep 15 '11 at 19:38
  • To be precise, what the second example says is that for any `b`, the first parameter is uniquely determined to be both `b` and `Bool`. The only use of `foo` that satisfies both constraints is when the instance needed is `Foo Bool Bool`, in which case it complains about the overlap, instead. – C. A. McCann Sep 15 '11 at 19:42
  • @C.A. McCann see my edit above. I know what the example *says* but I tested it and that isn't what it *does*. – sclv Sep 15 '11 at 19:54
  • See the edit to my answer as well. :] Those are copy+paste from GHCi. Using a `String` as the second argument gives me the same result that `()` did. What version of GHC are you using? – C. A. McCann Sep 15 '11 at 19:57
  • @C.A. McCann -- 7.0.4. Perhaps this is a bug that was fixed in later versions? – sclv Sep 15 '11 at 19:59
  • I was using 7.2.1, but can't find anything relevant in the release notes. Perhaps this was a bit of collateral repair from the changes that allowed equality constraints in class contexts? – C. A. McCann Sep 15 '11 at 20:03
  • @C.A. McCann -- hmmm. a quick crawl through the trac didn't lead any bug to jump out at me per se, but did reveal that the fundep solver has been through an overhaul or two since 7.0.4, so it could be any one of a number of things. In any case, we can leave it as a mystery mainly solved. – sclv Sep 15 '11 at 20:07
  • Interestingly, I get the same result on 6.10.4 as I do on 7.2.1. – C. A. McCann Sep 15 '11 at 20:08