我将尝试在这里提出一个“惯用的”singletons
解决方案(如果这样的事情甚至存在的话)。预赛:
{-# LANGUAGE
RankNTypes, DataKinds, PolyKinds, ConstraintKinds, GADTs,
TypeOperators, MultiParamTypeClasses, TypeFamilies, ScopedTypeVariables #-}
import Data.Singletons.Prelude
import Data.Proxy
import GHC.Exts (Constraint)
-- SingI constraint here for simplicity's sake
class SingI name => Analyze (name :: Symbol) ty where
analyze :: Proxy name -> ty -> Int
data Rec rs where
Nil :: Rec '[]
Cons :: ty -> Rec rs -> Rec ( '(name, ty) ': rs )
recName :: Rec ('(name, t) ': rs) -> Proxy name
recName _ = Proxy
我们需要一个All c rs
约束,但我们给它一个旋转并创建c
一个TyFun
普通的a -> Constraint
构造函数:
type family AllC (c :: TyFun a Constraint -> *) (rs :: [a]) :: Constraint where
AllC c '[] = ()
AllC c (x ': xs) = (c @@ x, AllC c xs)
TyFun
让我们对类型构造函数和类型族进行抽象,并为我们提供部分应用程序。它为我们提供了几乎一流的类型级函数,但语法有些难看。请注意,尽管我们必然会失去构造函数的注入性。@@
是应用TyFun
-s 的运算符。TyFun a b -> *
意味着那a
是输入并且b
是输出,而尾随-> *
只是编码的产物。有了 GHC 8.0,我们就可以做到
type a ~> b = TyFun a b -> *
并a ~> b
在之后使用。
我们现在可以实现一个通用的“经典”映射Rec
:
cMapRec ::
forall c rs r.
AllC c rs => Proxy c -> (forall name t. (c @@ '(name, t)) => Proxy name -> t -> r) -> Rec rs -> [r]
cMapRec p f Nil = []
cMapRec p f r@(Cons x xs) = f (recName r) x : cMapRec p f xs
请注意,上面c
有 kind TyFun (a, *) Constraint -> *
。
然后实施analyzeRec
:
analyzeRec ::
forall c rs. (c ~ UncurrySym1 (TyCon2 Analyze))
=> AllC c rs => Rec rs -> [(String, Int)]
analyzeRec = cMapRec (Proxy :: Proxy c) (\p t -> (fromSing $ singByProxy p, analyze p t))
首先,c ~ UncurrySym1 (TyCon2 Analyze)
它只是一个类型级别的let
绑定,让我可以将c
inProxy c
用作速记。(如果我真的想使用所有肮脏的技巧,我会添加{-# LANGUAGE PartialTypeSignatures #-}
并编写Proxy :: _ c
)。
UncurrySym1 (TyCon2 Analyze)
uncurry Analyze
如果 Haskell 完全支持类型级函数,它会做同样的事情。analyzeRec
这里的明显优势是我们可以在没有额外顶级类型族或类的情况下即时写出类型,并且使用AllC
更广泛。
作为奖励,让我们SingI
从 中删除约束Analyze
,并尝试实现analyzeRec
。
class Analyze (name :: Symbol) ty where
analyze :: Proxy name -> ty -> Int
现在我们必须要求一个额外的约束来表示我们的所有name
-s Rec
are SingI
。我们可以使用两个cMapRec
-s,然后压缩结果:
analyzeRec ::
forall analyze names rs.
(analyze ~ UncurrySym1 (TyCon2 Analyze),
names ~ (TyCon1 SingI :.$$$ FstSym0),
AllC analyze rs,
AllC names rs)
=> Rec rs -> [(String, Int)]
analyzeRec rc = zip
(cMapRec (Proxy :: Proxy names) (\p _ -> fromSing $ singByProxy p) rc)
(cMapRec (Proxy :: Proxy analyze) (\p t -> analyze p t) rc)
这里TyCon1 SingI :.$$$ FstSym0
可以翻译为SingI . fst
。
这仍然大致在可以用TyFun
-s 轻松表达的抽象级别内。当然,主要的限制是缺少 lambda。理想情况下,我们不必使用zip
,而是使用AllC (\(name, t) -> (SingI name, Analyze name t))
,并使用单个cMapRec
. 有了singletons
,如果我们不能再使用无点类型级编程来实现它,我们必须引入一个新的有点类型族。
有趣的是,GHC 8.0 将足够强大,以至于我们可以从头开始实现类型级别的 lambda,尽管它可能会很丑陋。例如,\p -> (SingI (fst p), uncurry Analyze p)
可能看起来像这样:
Eval (
Lam "p" $
PairL :@@
(LCon1 SingI :@@ (FstL :@@ Var "p")) :@@
(UncurryL :@@ LCon2 Analyze :@@ Var "p"))
其中所有L
后缀都表示普通 -s 的 lambda 项嵌入TyFun
(由 TH 生成的另一个简写集合......)。
我有一个原型,尽管由于 GHC 错误,它只适用于更丑陋的 de Bruijn 变量。它还具有Fix
类型级别的显式惰性。