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

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

Ответы

Ответ 1

Простейшая вещь, которую вы можете сделать, это

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, вы должны указать это значение более одного раза.

Вы можете избежать этого, а sabauma нужен для 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 a data вместо обертки newtype, вы можете расширить ее, указав при необходимости много полей в константе.

Ответ 2

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

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

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