Способ объявить постоянное значение в классе типа

Я хочу объявить класс типов, в котором есть некоторые реализованные функции, использующие нереализованное постоянное значение (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 = ?"

Идея проста: я хочу получить доступ к byId (и другим подобным функциям), создав экземпляр этого класса типов, указав только table:

instance StdQueries SomeType where
  table = "the_constant_value_for_this_type"

Но компилятор продолжает жаловаться на следующее сообщение:

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'

Есть ли решения такой проблемы? Может поможет обманка с newtype или что-то в этом роде?


person Nikita Volkov    schedule 29.11.2012    source источник


Ответы (2)


Самое простое, что вы можете сделать, это

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"

Это просто, но если у вас есть несколько функций, которым требуется доступ к значению table, вам придется указать это значение более одного раза.

Вы можете избежать этого, и сабаума нуждается в undefined и {-# LANGUAGE ScopedTypeVariables #-} следующим образом:

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"

Магия здесь заключается в сигнатуре типа для defaultById, которая заставляет byId предоставить table из того же экземпляра. Если бы мы предоставили defaultById :: (StdQueries a, StdQueries b) => Table a -> Int -> QueryM (Maybe b), то defaultById все равно скомпилировалось бы, но мы все равно получили бы сообщение об ошибке, похожее на то, что было в вашем вопросе: компилятор больше не знал бы, какое определение table использовать.

Сделав Table a структурой data вместо оболочки newtype, вы можете расширить это, чтобы указать много полей в константе, если это необходимо.

person dave4420    schedule 29.11.2012

Проблема в том, что в определении table не упоминаются какие-либо переменные типа класса, поэтому не было никакого способа выяснить, какую версию table использовать. Решение (правда, хакерское) может выглядеть примерно так:

{-# 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"

Который вы могли бы затем использовать через

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

Не то, чтобы я действительно рекомендовал делать это.

person sabauma    schedule 29.11.2012
comment
В этом нет ничего плохого, многие классы типов делают это именно так. - person leftaroundabout; 29.11.2012
comment
пожалуйста, отформатируйте свой код, чтобы сделать его более читабельным; Кроме того, это действительно работает? похоже, что вы сейчас делаете table функцией, но вы не используете ее таким образом в части where sql = .... - person ErikR; 29.11.2012
comment
тип таблицы a -> String, а не String, так как вы его объединяете? - person Satvik; 29.11.2012
comment
вы должны сделать table (undefined :: a) и использовать расширение ScopedTypeVariables - person Satvik; 29.11.2012
comment
@Satvik Забыл исправить использование table в определении класса. Теперь его можно использовать. - person sabauma; 29.11.2012
comment
@leftaroundabout Я не знал об этом. Мне просто становится не по себе, когда я вижу, как undefined плавает вокруг. - person sabauma; 29.11.2012
comment
@sabauma: Если вы действительно хотите, вы можете пройти через data Proxy a = Proxy, что даст вам table :: Proxy a -> String и table (Proxy :: Proxy SomeType). Немного более многословно, но намного безопаснее. - person Vitus; 29.11.2012
comment
Функция sizeOf из Storable, вероятно, является наиболее распространенным примером передачи аргумента для определения типа. crypto-api использует решение Proxy (через библиотеку tagged) для решения этой проблемы. - person Thomas M. DuBuisson; 30.11.2012