Как объединить кортежи в фантомные типы в 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 решает очень похожую проблему и, возможно, стоит посмотреть.

Другие вопросы по тегам