ここでは、「慣用的な」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
型コンストラクターと型ファミリーを抽象化し、部分的な適用を可能にします。これにより、構文がいくぶん見苦しい、ほとんどファーストクラスの型レベル関数が得られます。ただし、必然的にコンストラクターの注入性が失われることに注意してください。-s@@
を適用するための演算子です。それが入力であり出力であることを意味し、末尾は単なるエンコーディングのアーティファクトです。GHC 8.0 でできることTyFun
TyFun a b -> *
a
b
-> *
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))
1つ目は、略記としてinを使用できるc ~ UncurrySym1 (TyCon2 Analyze)
型レベルのlet
バインディングです。(すべての汚いトリックを本当に使用したい場合は、 を追加して記述します)。c
Proxy 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
ますSingI
。cMapRec
2 つの-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で簡単に表現できる抽象化のレベルの範囲内です。もちろん、主な制限はラムダがないことです。zip
を使用する必要はなく、代わりに をAllC (\(name, t) -> (SingI name, Analyze name t))
使用し、単一の を使用するのが理想的cMapRec
です。ではsingletons
、ポイントフリーの型レベル プログラミングでこれ以上うまくいかない場合は、新しいポイントフル型ファミリを導入する必要があります。
面白いことに、GHC 8.0 は十分に強力なので、型レベルのラムダをゼロから実装することができますが、それはおそらく地獄のように醜いものになるでしょう。たとえば、\p -> (SingI (fst p), uncurry Analyze p)
次のようになります。
Eval (
Lam "p" $
PairL :@@
(LCon1 SingI :@@ (FstL :@@ Var "p")) :@@
(UncurryL :@@ LCon2 Analyze :@@ Var "p"))
ここで、すべてのL
接尾辞は、通常の -s のラムダ項の埋め込みを示しますTyFun
(TH によって生成されるさらに別の短縮形のコレクション...)。
私はプロトタイプを持っていますが、GHC のバグのため、さらに醜い de Bruijn 変数でしか動作しません。Fix
また、型レベルでの明示的な遅延も特徴としています。