Как объединить кортежи в фантомные типы в Haskell?
Я пишу комбинатор SQL, который позволяет составлять фрагменты SQL как моноиды. У меня есть примерно такой тип (это упрощенная реализация):
data SQLFragment = { selects :: [String], froms :[String], wheres :: [String]}
instance Monoid SQL Fragment where ...
Это позволяет мне легко комбинировать биты SQL, которые я часто использую, и делать такие вещи, как:
email = select "email" <> from "user"
name = select "name" <> from "user"
administrators = from "user" <> where_ "isAdmin = 1"
toSql $ email <> name <> administrators
=> "SELECT email, name FROM user WHERE isAdmin = 1"
Это работает очень хорошо, и я доволен этим. Сейчас пользуюсь MySQL.Simple
и чтобы быть выполненным, он должен знать тип строки.
main = do
conn <- SQL.connect connectInfo
rows <- SQL.query_ conn $ toSql (email <> name <> administrators)
forM_ (rows :: [(String, String)]) print
Вот почему мне нужно
rows :: [(String, String)]
Чтобы не добавлять вручную эту явную (и бесполезную) подпись типа, у меня была следующая идея: я добавляю фантомный тип в свой SQLFragment
и использовать его, чтобы заставить тип query_
функция. Так что я мог бы иметь что-то вроде этого
email = select "email" <> from "user" :: SQLFragment String
name = select "name" <> from "user" :: SQLFragment String
administrators = from "user" <> where_ "isAdmin = 1" :: SQLFragment ()
так далее...
Тогда я могу сделать
query_ :: SQL.Connection -> SQLFragment a -> IO [a]
query_ con q = SQL.query_ conn (toSql q)
Моя первая проблема - я не могу использовать <>
больше потому что SQLFragment a
это не Monoid
больше. Во-вторых, как я могу реализовать свой новый <>
правильно рассчитать фантомный тип?
Я нашел способ, который я считаю уродливым, и я надеюсь, что есть гораздо лучшее решение. Я создал typed version
из SQLFragment
и использовать фантомный атрибут, который является HList
,
data TQuery e = TQuery
{ fragment :: SQLFragment
, doNotUse :: e
}
тогда я создаю новый typed
оператор: !<>!
который я не понимаю сигнатуру типа, поэтому я не пишу это
(TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q') (e.*.e')
Теперь я не могу объединить свой напечатанный фрагмент и отслеживать тип (хотя это еще не кортеж, а что-то действительно странное).
Чтобы преобразовать этот странный тип в кортеж, я создаю семейство типов:
type family Result e :: *
и создать его для некоторых кортежей
Другое решение было бы, вероятно, использовать семейство типов и писать вручную каждую комбинацию кортежей
type instance Result (HList '[a]) = (SQL.Only a)
type instance Result (HList '[HList '[a], b]) = (a, b)
type instance Result (HList '[HList '[HList '[a], b], c]) = (a, b, c)
type instance Result (HList '[HList '[HList '[HList '[a], b], c], d]) = (a, b, c, d)
type instance Result (HList '[HList '[HList '[HList '[HList '[a], b], c], d], e]) = (a, b, c,d, e)
так далее...
И это работает. Я могу написать свою функцию, используя Result
семья
execute :: (SQL.QueryResults (Result e)) =>
SQL.Connection -> TQuery e -> SQL.Connection -> IO [Result e]
execute conn (TQuery q _ ) = SQL.query_ conn (toSql q)
Моя основная программа выглядит так:
email = TQuery (select "email" <> from "user") ((undefined :: String ) .*. HNil)
name = TQuery (select "name" <> from "user" ) ((undefined :: String ) .*. HNil)
administrators = TQuery (from "user" <> where_ "isAdmin = 1") (HNil)
main = do
conn <- SQL.connect connectInfo
rows <- execute conn $ email !<>! name !<>! administrators
forM_ rows print
и это работает!
Однако есть ли лучший способ сделать это, особенно без использования HList
и если возможно меньше расширений, насколько это возможно?
Если я как-то "спрятал" фантомный тип (чтобы у меня был настоящий Monoid
и быть в состоянии использовать <>
вместо !<>!
) есть ли способ вернуть тип?
1 ответ
Подумайте об использовании haskelldb, в котором решена проблема типизированных запросов к базе данных. Записи в haskelldb работают нормально, но они не предоставляют много операций, а типы длиннее, так как они не используют -XDataKinds
,
У меня есть несколько предложений для вашего текущего кода:
newtype TQuery (e :: [*]) = TQuery SQLFragment
лучше, потому что e
на самом деле фантомный тип. Тогда ваша операция добавления может выглядеть так:
(!<>!) :: TQuery a -> TQuery b -> TQuery (HAppendR a b)
TQuery a !<>! TQuery b = TQuery (a <> b)
Result
тогда выглядит намного чище:
type family Result (a :: [*])
type instance Result '[a]) = (SQL.Only a)
type instance Result '[a, b] = (a, b)
type instance Result '[a, b, c] = (a, b, c)
type instance Result '[a, b, c, d] = (a, b, c, d)
type instance Result '[a, b, c, d, e] = (a, b, c,d, e)
-- so you might match the 10-tuple mysql-simple offers
Если вы хотите остаться с HList+mysql-simple и дублировать части haskelldb, instance QueryResults (Record r)
вероятно уместно. Неизданный экземпляр Read решает очень похожую проблему и, возможно, стоит посмотреть.