6

我正在编写一个 SQL 组合器,它允许将 SQL 片段组合为 Monoid。我大致有这样的类型(这是一个简化的实现):

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 ()

ETC ...

那我可以做

query_ :: SQL.Connection -> SQLFragment a -> IO [a]
query_ con q = SQL.query_ conn (toSql q)

我的第一个问题是我不能再使用<>了,因为不再SQLFragment a是 aMonoid了。第二个是如何实现我的 new<>以正确计算幻像类型?

我找到了一种我认为丑陋的方法,我希望有一个更好的解决方案。我创建了一个typed versionofSQLFragment并使用了一个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)

ETC ...

那行得通。我可以使用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并且能够使用<>而不是!<>!)有没有办法让这个类型回来?

4

1 回答 1

2

考虑使用已找出类型数据库查询问题的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 的重复部分,aninstance QueryResults (Record r)可能是合适的。未发布的Read 实例解决了一个非常相似的问题,可能值得一看。

于 2014-06-05T07:32:56.177 回答