How can I use HaskellDB with polymorphic fields? (Problems with overlapping instances)

I have a schema which has 6 different types of entities, but they all have a lot of things in common. I figured I could probably abstract a lot of this commonality out at the type level, but I've hit a problem with HaskellDB and overlapping instances. Here's the code I started with, which works fine:

import Database.HaskellDB
import Database.HaskellDB.DBLayout

data Revision a = Revision deriving Eq
data Book = Book

instance FieldTag (Revision a) where
  fieldName _ = "rev_id"

revIdField :: Attr (Revision Book) (Revision Book)
revIdField = mkAttr undefined

branch :: Table (RecCons (Revision Book) (Expr (Revision Book)) RecNil)
branch = baseTable "branch" $ hdbMakeEntry undefined
bookRevision :: Table (RecCons (Revision Book) (Expr (Revision Book)) RecNil)
bookRevision = baseTable "book_revision" $ hdbMakeEntry undefined

masterHead :: Query (Rel (RecCons (Revision Book) (Expr (Revision Book)) RecNil))
masterHead = do
  revisions <- table bookRevision
  branches <- table branch
  restrict $ revisions ! revIdField .==. branches ! revIdField
  return revisions

This works fine, but branch is too specific. What I actually want to express is the following:

branch :: Table (RecCons (Revision entity) (Expr (Revision entity)) RecNil)
branch = baseTable "branch" $ hdbMakeEntry undefined

However, with this change, I get the following error:

Overlapping instances for HasField
                            (Revision Book)
                            (RecCons (Revision entity0) (Expr (Revision entity0)) RecNil)
  arising from a use of `!'
Matching instances:
  instance [overlap ok] HasField f r => HasField f (RecCons g a r)
    -- Defined in Database.HaskellDB.HDBRec
  instance [overlap ok] HasField f (RecCons f a r)
    -- Defined in Database.HaskellDB.HDBRec
(The choice depends on the instantiation of `entity0'
 To pick the first instance above, use -XIncoherentInstances
 when compiling the other instance declarations)
In the second argument of `(.==.)', namely `branches ! revIdField'
In the second argument of `($)', namely
  `revisions ! revIdField .==. branches ! revIdField'
In a stmt of a 'do' expression:
      restrict $ revisions ! revIdField .==. branches ! revIdField

I've tried blindly throwing -XOverlappingInstances and -XIncoherentInstances at this, but that doesn't help (and I'd like to actually understand why replacing a concrete type with a type variable causes this to be so problematic).

Any help and advice would be much appreciated!


Solution 1:

With the age of this question, it is probably too late of an answer to make any difference to you, but maybe if some other person comes along with a similar problem...

It comes down to the fact that it cannot be inferred that you want entity to refer to Book when branch is used in masterHead. The part of the error message that reads

The choice depends on the instantiation of `entity0'

tells you where you need to remove the ambiguity, specifically that you need to give more information about what entity0 should be. You can give some type annotations to help things out.

First, define branch as

type BranchTable entity = Table (RecCons (Revision entity) (Expr (Revision entity)) RecNil)
branch :: BrancTable entity
branch = baseTable "branch" $ hdbMakeEntry undefined

and then change masterHead to read

masterHead :: Query (Rel (RecCons (Revision Book) (Expr (Revision Book)) RecNil))
masterHead = do
  revisions <- table bookRevision
  branches <- table (branch :: BranchTable Book)
  restrict $ revisions ! revIdField .==. branches ! revIdField
  return revisions

Note the type annotation applied to branch: branch :: BranchTable Book which serves to remove the ambiguity that was causing the type error.

To make masterHead applicable to anything with a Revision e field in it, you can use this definition:

masterHead :: (ShowRecRow r, HasField (Revision e) r) => Table r -> e -> Query (Rel r)
masterHead revTable et =
  do  revisions <- table revTable
      branches <- table branch'
      restrict $ revisions ! revIdField' .==. branches ! revIdField'
      return revisions
  where (branch', revIdField') = revBundle revTable et
        revBundle :: HasField (Revision e) r => Table r -> e -> (BranchTable e, Attr (Revision e) (Revision e))
        revBundle table et = (branch, revIdField)

The et argument is needed to specify what the e type should be and can just be undefined ascribed to the proper type as in

masterHead bookRevision (undefined :: Book)

which generates the SQL

SELECT rev_id1 as rev_id
FROM (SELECT rev_id as rev_id2
      FROM branch as T1) as T1,
     (SELECT rev_id as rev_id1
      FROM book_revision as T1) as T2
WHERE rev_id1 = rev_id2

this does require FlexibleContexts though, but it can be applied to the asker's module without recompiling HaskellDB.