我正在编写一个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 ()
等等......
然后我可以做
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
并且能够使用<>
代替!<>!
)是否有办法获取该类型回来?
答案 0 :(得分: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的重复部分,instance QueryResults (Record r)
可能是合适的。一个未发布的Read instance解决了一个问题
类似的问题,可能值得一看。