Как объединить кортежи фантомных типов в 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 и я смогу использовать <> вместо !<>!), есть ли способ вернуть этот тип?


person mb14    schedule 04.06.2014    source источник
comment
Что не так с написанием вторичной функции strQuery :: Connection -> Query -> IO [(String, String)]; strQuery = SQL.query_, очень похоже на написание функции readInt :: String -> Int; readInt = read? Если вы всегда получаете один и тот же возвращаемый тип или только один из нескольких типов, тогда этот подход должен быть довольно управляемым и не требует для работы какой-либо сложной сантехники. В противном случае я не вижу проблем с указанием встроенного типа.   -  person bheklilr    schedule 04.06.2014
comment
в (TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q) (e.*.e') может ты имел ввиду q<>q'?   -  person didierc    schedule 04.06.2014
comment
@didierc: Да, конечно. Я починил это. Спасибо.   -  person mb14    schedule 05.06.2014
comment
@bheklir: Что не так в добавлении сигнатуры типа, так это то, что она избыточна и теоретически не нужна, каждый раз, когда я изменяю запрос, мне нужно изменять сигнатуру типа (если мне нужно изменить что-то в двух местах, это избыточно). Более того, любая сигнатура типа будет проверять тип, но может дать сбой во время выполнения. С моим водопроводчиком, как только столбец был правильно объявлен (что было сделано один раз и который я сгенерировал из схемы), все комбинации запросов, которые проверяют тип, будут работать. Так что в основном это тип безопаснее.   -  person mb14    schedule 05.06.2014
comment
Меня очень смущают тип TQuery, оператор !<>! и оператор .*., который я не могу найти с помощью hoogle. Могли бы вы объяснить?   -  person didierc    schedule 05.06.2014
comment
@didierc: Google не работает, но Хаю нашел его. .*. происходит из HList, это гетерогенная версия :. Добавьте что-нибудь в HList.   -  person mb14    schedule 05.06.2014


Ответы (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). Невыпущенный экземпляр чтения решает очень похожую проблему, и, возможно, стоит посмотреть .

person aavogt    schedule 05.06.2014
comment
Это действительно намного чище и в значительной степени то, что я ищу, хотя я бы предпочел полностью избавиться от HList. Я рассматривал HaskellDB, а также Esqueletto, но ни один из них не был тем, что мне нужно для того, что я пытаюсь сделать, что в основном не требует указания предложения from и автоматического соединения. - person mb14; 05.06.2014