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

mb1*_*b14 6 haskell hlist

我正在编写一个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 versionSQLFragment,用幻影的属性,它是一个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并且能够使用<>而不是!<>!)有没有办法让类型恢复?

aav*_*ogt 2

考虑使用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 实例解决了非常相似的问题,可能值得一看。