2

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

Nick Tchayka
  • 563
  • 3
  • 14
  • It will "inherit" the constraint, like `instance IsRecord Bar => Foo Bar`. – Willem Van Onsem Jun 14 '19 at 09:22
  • Yeah, but that shouldn't compile, right? – Nick Tchayka Jun 14 '19 at 09:23
  • 1
    why not? The constraint is false, but that is not a problem. If you later add the instance, in a module that uses this, then it becomes "available". – Willem Van Onsem Jun 14 '19 at 09:25
  • I understand that in practice this is not a problem, but my question goes towards *what* is the difference between those two things. – Nick Tchayka Jun 14 '19 at 09:27
  • 3
    that you simply say that *if* `IsRecord Bar` holds, then we can define an `instance Foo Bar` as specified in that instance. For now that constraint does not hold, if you later add an instance, for example in *another* module, then the constraint is true, and hence you get an instance. It thus adds extra flexibility. – Willem Van Onsem Jun 14 '19 at 09:32
  • For example in the source code there are instance declarations like `instance HasCallStack => PrintStack` that are thus available, given the system uses a call stack. This instance is thus only avaiable then. – Willem Van Onsem Jun 14 '19 at 09:38
  • I see now, thanks! If you could post an answer, I can mark it as the solution. @WillemVanOnsem – Nick Tchayka Jun 14 '19 at 10:49

1 Answers1

1

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

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380