Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A way to declare a constant value in a type class

I want to declare a typeclass that has some implemented functions which utilize an unimplemented constant value (table):

class FromRow a => StdQueries a where
  table :: String
  byId :: Int -> QueryM (Maybe a)
  byId = fmap listToMaybe . queryM sql . Only
    where sql = read $ "SELECT * FROM " ++ table ++ " WHERE id = ?"

The idea is simple: I want to get the byId (and other similar functions) available by instantiating this typeclass by specifying just the table:

instance StdQueries SomeType where
  table = "the_constant_value_for_this_type"

But the compiler keeps complaining with the following message:

The class method `table'
mentions none of the type variables of the class StdQueries a
When checking the class method: table :: String
In the class declaration for `StdQueries'

Are there any solutions to that kind of a problem? Can tricking with newtype help or anything like that?

like image 492
Nikita Volkov Avatar asked Nov 29 '12 15:11

Nikita Volkov


People also ask

How do you declare a class constant?

A class constant is declared inside a class with the const keyword. Class constants are case-sensitive. However, it is recommended to name the constants in all uppercase letters.

How do you declare a constant value?

Variables can be declared as constants by using the “const” keyword before the datatype of the variable. The constant variables can be initialized once only. The default value of constant variables are zero.

How do you declare a class constant in Java?

To turn an ordinary variable into a constant, you have to use the keyword "final." As a rule, we write constants in capital letters to differentiate them from ordinary variables. If you try to change the constant in the program, javac (the Java Compiler) sends an error message.

How do you declare a constant in a class in C ++?

To declare a constant variable in C++, the keyword const is written before the variable's data type. Constant variables can be declared for any data types, such as int , double , char , or string .


2 Answers

The simplest thing you can do is

class FromRow a => StdQueries a where
    byId :: Int -> QueryM (Maybe a)

defaultById :: FromRow a => String -> Int -> QueryM (Maybe a)
defaultById table = fmap listToMaybe . queryM sql . Only
    where sql = read $ "SELECT * FROM " ++ table ++ " WHERE id = ?"

instance StdQueries SomeType where
    byId = defaultById "the_constant_value_for_this_type"

This is simple, but if you have more than one function that needs access to the table value, you have to specify that value more than once.

You can avoid that, and sabauma's need for undefined and {-# LANGUAGE ScopedTypeVariables #-} like this:

newtype Table a = Table String

class FromRow a => StdQueries a where
    table :: Table a
    byId :: Int -> QueryM (Maybe a)
    byId = defaultById table

defaultById :: StdQueries a => Table a -> Int -> QueryM (Maybe a)
defaultById (Table table) = fmap listToMaybe . queryM sql . Only
    where sql = read $ "SELECT * FROM " ++ table ++ " WHERE id = ?"

instance StdQueries SomeType where
    table = Table "the_constant_value_for_this_type"

The magic here is the type signature for defaultById, which forces byId to provide the table from the same instance. If we provided defaultById :: (StdQueries a, StdQueries b) => Table a -> Int -> QueryM (Maybe b) then defaultById would still compile, but we would still get a similar error message to the one in your question: the compiler would no longer know which definition of table to use.

By making Table a a data structure instead of a newtype wrapper, you can extend this to specify many fields in the constant, if required.

like image 129
dave4420 Avatar answered Nov 04 '22 07:11

dave4420


The issue is that the definition of table does not mention any of the type variables of the class, so there would not be any way to figure out which version of table to use. A (admittedly hackish) solution might be something like:

{-# LANGUAGE ScopedTypeVariables #-}
class FromRow a => StdQueries a where
  table :: a -> String
  byId :: Int -> QueryM (Maybe a)
  byId = fmap listToMaybe . queryM sql . Only
    where sql = read $ "SELECT * FROM " ++ table (undefined :: a) ++ " WHERE id = ?"

instance StdQueries SomeType where
    table = const "the_constant_value_for_this_type"

Which you could then use via

table (undefined :: SomeType) == "the_constant_value_for_this_type"

Not that I would really recommend doing this.

like image 26
sabauma Avatar answered Nov 04 '22 07:11

sabauma