Search code examples
haskellghctypeclasstype-familiesderiving

DeriveAnyClass vs Empty Instance


Suppose that I have this type family that throws a custom type error during compile time if the type passed to it is not a record:

type family IsRecord (a :: Type) where
  ...

Now I have this type class that has methods with default implementations, but requires that the type is a record, by adding the IsRecord constraint:

class IsRecord a => Foo a where
  foo :: Text
  foo = "foo"

When trying to use it incorrectly, if we use it as a regular instance with a type that is not a record, it successfully fails to compile:

data Bar = Bar

instance Foo Bar   -- error: Bar is not a record

But if I enable -XDeriveAnyClass and add it to the deriving clause, this doesn't fail to compile, ignoring completely the constraint:

data Bar = Bar
  deriving (Foo)

I understand that DeriveAnyClass generates an empty instance declaration, which is what I'm doing on the first example, but still it doesn't throw the error. What's going on?

I'm using GHC 8.6.4


Solution

  • Wow! I was going to mark this as a duplicate of What is the difference between DeriveAnyClass and an empty instance?, but it seems the behavior of GHC has changed since that question was asked and answered!

    Anyway, if you ask -- either with :i inside ghci or with -ddump-deriv before starting ghci -- what the compiler has done, it's clear what the difference is in your case:

    > :i Bar
    data Bar = Bar  -- Defined at test.hs:15:1
    instance IsRecord Bar => Foo Bar -- Defined at test.hs:16:13
    

    Indeed, if you change the non-DeriveAnyClass version of your code to match, writing

    instance IsRecord Bar => Foo Bar
    

    instead of

    instance Foo Bar
    

    everything works fine. The details of how this instance context was chosen seem a bit complicated; you can read what the GHC manual has to say about it here, though I suspect the description there is either not quite precise or not complete, as I don't get the same answer the compiler does here if I strictly follow the rules stated at the documentation. (I suspect the true answer is that it writes the instance first, then just does the usual type inference thing and copies any constraints it discovers in that way into the instance context.)