如何在 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)
我的第一个问题是我不能再使用<>
,因为SQLFragment a
不再是Monoid
。
第二个是如何实现我的新<>
以正确计算幻像类型?
我发现了一种我认为丑陋的方法,我希望有更好的解决方案。
我创建了SQLFragment
的typed version
并使用了HList
的幻像属性。
data TQuery e = TQuery
fragment :: SQLFragment
, doNotUse :: e
然后我创建一个新的typed
运算符:!<>!
,我不了解类型签名,所以我不写它
(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
并能够使用<>
而不是!<>!
)有没有办法恢复该类型?
【问题讨论】:
写辅助函数有什么问题strQuery :: Connection -> Query -> IO [(String, String)]
; strQuery = SQL.query_
,很像写函数readInt :: String -> Int
; readInt = read
?如果您总是得到相同的返回类型,或者只是少数几种类型中的一种,那么这种方法应该非常易于管理,并且不需要任何花哨的管道即可工作。否则,我认为必须指定内联类型没有问题。
in (TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q) (e.*.e')
也许你的意思是q<>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。我一直在考虑HaskellDB
和Esqueletto
,但它们都不是我想要做的事情所需要的,这基本上不必指定“from”子句并进行自动加入。 以上是关于如何在 Haskell 中连接幻像类型中的元组?的主要内容,如果未能解决你的问题,请参考以下文章