1
votes

I'm testing some code for a little experiment I'm doing but right at the beginning I've hit a roadblock that I don't see how to fix.

data DatabaseDriver a b where
  DatabaseDriver :: (Table table, Field field) =>  {
      dbInsert :: table -> [field] -> String
    , dbSelect :: table -> [field] -> String
    } -> DatabaseDriver a b

class Table a where
    tableName :: a -> String

class Field a where
    fieldName :: a -> String
    fieldValue :: a -> String

psqlDriver = DatabaseDriver insert select
    where
        insert t fs = "insert into " ++ tableName t ++ " (" ++ fieldNames fs ++ ") values (" ++ fieldValues fs ++ ")"
        select t fs = "select " ++ fieldNames fs ++ " from " ++ tableName t
        fieldNames = joinComma fieldName
        fieldValues = joinComma fieldValue
        joinComma f = foldl (\a n -> a ++ ", " ++ n) "" . map f

Ok, so this is some test code, the driver functions would get much more complicated than this but even in this test I get the error "Ambiguous type variable 'a0' in the constraint: (Field a0) arising from a use of `fieldName'. So the compiler does see that fieldName is applied to a field but apparently it wants a more concrete type here. I guess having the functions remain polymorphic makes pgsqlDriver not a concrete class?

But the idea would be that these functions are polymorphic. That's the reason I chose to use a GADT here, so I could put type constraints on the parameters to these driver functions without having to repeat them in every instantiation. The plan would be that the defined database driver could work with any Field and Table instances. Can this simply not be done and my DatabaseDriver type would also have to be a type class?

1

1 Answers

3
votes

There are three options here. The first option is to add

{-# LANGUAGE NoMonomorphismRestriction #-}

to the top of your module file.

The second option is to add an explicit type signature to psqlDriver.

psqlDriver :: (Field field, Table table) => (table -> [field] -> String) -> DatabaseDriver a b

The reason for all of this is a little nuanced, and more details can be found here

The third option is to change the definition of psqlDriver to

psqlDriver = DatabaseDriver insert select

However, this is truly ambiguous - there is not reason to prefer any particular instance of Table or Field over another. Perhaps you mean to define DatabaseDriver as follows.

data DatabaseDriver table field where
  DatabaseDriver :: (Table table, Field field) =>  {
      dbInsert :: table -> [field] -> String
    , dbSelect :: table -> [field] -> String
    } -> DatabaseDriver table field

If the original definition of DatabaseDriver is rewritten as an ADT, it is more obvious why.

As it is currently written in the question, the translation to an ADT is

{-# LANGUAGE ExistentialQuantification #-}

data DatabaseDriver a b
  = forall table field .
    (Table table, Field field) => DatabaseDriver
    { dbInsert :: table -> [field] -> String
    , dbSelect :: table -> [field] -> String
    }

Notice the nested forall table field, and how table and field have no relationship to a or b.

The intended translation is either

data DatabaseDriver table field
  = (Table table, Field field) => DatabaseDriver
    { dbInsert :: table -> [field] -> String
    , dbSelect :: table -> [field] -> String
    }

or most likely

data DatabaseDriver table field
  = DatabaseDriver
    { dbInsert :: table -> [field] -> String
    , dbSelect :: table -> [field] -> String
    }

Having the type class constraints in the definition of DatabaseDriver does not let you remove the type class constraints from any use of DatabaseDriver, in particular psqlDriver. In both of the ADT translations above, the type of psqlDriver is

psqlDriver :: (Table table, Field field) => DatabaseDriver table field