我正在编写一个SQL组合器,它允许将SQL片段组成一个Monoid.我有一个这样的类型(这是一个简化的实现):
data SQLFragment = { selects :: [String], froms :[String], wheres :: [String]}
instance Monoid SQL Fragment where ...
Run Code Online (Sandbox Code Playgroud)
这允许我轻松地结合我经常使用的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"
Run Code Online (Sandbox Code Playgroud)
这非常有效,我很满意.现在我使用MySQL.Simple并执行它需要知道行的类型.
main = do
conn <- SQL.connect connectInfo
rows <- SQL.query_ conn $ toSql (email <> name <> administrators)
forM_ (rows :: [(String, String)]) print
Run Code Online (Sandbox Code Playgroud)
这就是我需要的原因
rows :: [(String, String)]
Run Code Online (Sandbox Code Playgroud)
为了避免手动添加这个显式(和无用)类型签名,我有以下想法:我向我添加一个幻像类型SQLFragment并使用它来强制query_函数的类型.所以我可以有类似的东西
email = select "email" <> from "user" :: SQLFragment String
name = select "name" <> from "user" :: SQLFragment String
administrators = from "user" <> where_ "isAdmin = 1" :: SQLFragment ()
Run Code Online (Sandbox Code Playgroud)
等......
然后我就能做到
query_ :: SQL.Connection -> SQLFragment a -> IO [a]
query_ con q = SQL.query_ conn (toSql q)
Run Code Online (Sandbox Code Playgroud)
我的第一个问题是我不能再使用<>因为SQLFragment a不再是Monoid了.第二个是如何实现我的新功能<>以正确计算幻像类型?
我发现了一种我认为很难看的方法,我希望有一个更好的解决方案.我创建了一个typed version的SQLFragment,用幻影的属性,它是一个HList.
data TQuery e = TQuery
{ fragment :: SQLFragment
, doNotUse :: e
}
Run Code Online (Sandbox Code Playgroud)
然后我创建一个新的typed运算符:!<>! 我没有看不到类型签名所以我不写它
(TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q') (e.*.e')
Run Code Online (Sandbox Code Playgroud)
现在我无法组合我的类型片段并跟踪类型(即使它不是一个元组但是真的很奇怪).
要将这种奇怪的类型转换为元组,我创建了一个类型族:
type family Result e :: *
Run Code Online (Sandbox Code Playgroud)
并为一些元组实例化它
另一种解决方案可能是使用类型族并手动编写元组的每个组合
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)
Run Code Online (Sandbox Code Playgroud)
等......
这很有效.我可以用Result家人写我的功能
execute :: (SQL.QueryResults (Result e)) =>
SQL.Connection -> TQuery e -> SQL.Connection -> IO [Result e]
execute conn (TQuery q _ ) = SQL.query_ conn (toSql q)
Run Code Online (Sandbox Code Playgroud)
我的主程序如下:
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
Run Code Online (Sandbox Code Playgroud)
它的工作原理!
但是有没有更好的方法来做到这一点,特别是HList如果不尽可能不使用和尽可能少的扩展?
如果我以某种方式"隐藏"幻像类型(因此我可以拥有一个真实的Monoid并且能够使用<>而不是!<>!)有没有办法让类型恢复?
考虑使用haskelldb,它已经解决了类型化数据库查询问题。haskelldb 中的记录工作正常,但它们不提供很多操作,并且类型较长,因为它们不使用-XDataKinds.
我对您当前的代码有一些建议:
newtype TQuery (e :: [*]) = TQuery SQLFragment
Run Code Online (Sandbox Code Playgroud)
更好,因为它e实际上是幻像类型。那么你的追加操作可能如下所示:
(!<>!) :: TQuery a -> TQuery b -> TQuery (HAppendR a b)
TQuery a !<>! TQuery b = TQuery (a <> b)
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
如果您想保留 HList+mysql-haskelldb 的简单和重复部分,那么 aninstance QueryResults (Record r)可能是合适的。未发布的Read 实例解决了非常相似的问题,可能值得一看。