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

Posted

技术标签:

【中文标题】如何在 Haskell 中连接幻像类型中的元组?【英文标题】:How concatenate tuples in phantom types in Haskell? 【发布时间】:2014-06-04 17:18:06 【问题描述】:

我正在编写一个 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)

我的第一个问题是我不能再使用&lt;&gt;,因为SQLFragment a 不再是Monoid。 第二个是如何实现我的新&lt;&gt; 以正确计算幻像类型?

我发现了一种我认为丑陋的方法,我希望有更好的解决方案。 我创建了SQLFragmenttyped version 并使用了HList 的幻像属性。

data TQuery e = TQuery 
                fragment :: SQLFragment
               , doNotUse :: e
               

然后我创建一个新的typed 运算符:!&lt;&gt;!,我不了解类型签名,所以我不写它

(TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q') (e.*.e')

现在我无法组合我的 typed 片段并跟踪类型(即使它还不是元组,但确实很奇怪)。

为了将这种奇怪的类型转换为元组,我创建了一个类型族:

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 family 编写我的函数

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 并能够使用&lt;&gt; 而不是!&lt;&gt;!)有没有办法恢复该类型?

【问题讨论】:

写辅助函数有什么问题strQuery :: Connection -&gt; Query -&gt; IO [(String, String)]; strQuery = SQL.query_,很像写函数readInt :: String -&gt; IntreadInt = read?如果您总是得到相同的返回类型,或者只是少数几种类型中的一种,那么这种方法应该非常易于管理,并且不需要任何花哨的管道即可工作。否则,我认为必须指定内联类型没有问题。 in (TQuery q e) !&lt;&gt;! (TQuery q' e') = TQuery (q&lt;&gt;q) (e.*.e') 也许你的意思是q&lt;&gt;q' @didierc:是的,当然。我修好了它。谢谢。 @bheklir:添加类型签名的问题在于它是多余的,理论上是不必要的,每次修改查询时,我都需要修改类型签名(如果我需要在 2地方它是多余的)。此外,任何类型签名都会进行类型检查,但可能会在运行时失败。使用我的管道,一旦正确声明了一个列(它只完成了一次并且我从架构中生成),每个类型检查的查询组合都将起作用。所以它基本上是“类型”更安全。 @didierc:Hoogle 不起作用,但 Hayoo 找到了。 .*. 来自 HList ,是: 的异构版本。在 HList 中添加一些内容。 【参考方案1】:

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

【讨论】:

这确实更干净,几乎是我正在寻找的东西,尽管我更愿意完全摆脱 HList。我一直在考虑HaskellDBEsqueletto,但它们都不是我想要做的事情所需要的,这基本上不必指定“from”子句并进行自动加入。

以上是关于如何在 Haskell 中连接幻像类型中的元组?的主要内容,如果未能解决你的问题,请参考以下文章

列表理解列表中的元组列表

识别 Haskell 元组中的重复项

Python中的元组(Tuple)

如何从python中的列表中删除重复的元组?

我如何获得haskell中元组的索引?

在 Haskell 中优化部分计算