如何在Haskell中连接幻像类型中的元组?

时间:2014-06-04 17:18:06

标签: haskell hlist

我正在编写一个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并且能够使用<>代替!<>!)是否有办法获取该类型回来?

1 个答案:

答案 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解决了一个问题 类似的问题,可能值得一看。