引用モナド
テンプレートは
Q
モナドにラップされた値を返さなければならないので、このために、
Exp
、
Lit
、
Pat
型のコンストラクターを「レイズ」(
Q
ラップ)する一連の補助関数があります:
lamE
(
LamE
)、
varE
、
appE
、
varP
などd。 署名では、再指定された隆起型も使用されます:
ExpQ = Q Exp
、
LitQ = Q Lit
、
PatQ = Q Pat
...(これらはすべて
Language.Haskell.TH.Lib
モジュールにあります)。 これらの関数を使用すると、do-syntaxを使用する頻度が少なくなり、コードを大幅に削減できます。
THには、
Lift
クラスの任意のタイプの値を
Q Exp
上げる
lift
機能もあります。
まれに、一意の名前を生成する必要はなく、外部(テンプレートに関連する)コードからの正確な識別子名を使用する必要があります。 これらの目的のために、(純粋な)関数
mkName
∷ String → Name
ます。 ヘルパー関数
dyn
s = return (VarE (mkName s))
もあります。これは、指定された名前(
dyn ∷ String → Q Exp
)を持つ変数を表す
Exp
値を返します。
引用符ブラケット
抽象構文ツリーを表す
Exp
値の構築は、時間がかかり、退屈な仕事です。 しかし幸いなことに、テンプレートHaskellには、特定のHaskellコードをそれを表す構造に変換する引用符があります。
次の4つのタイプがあります。
-
[e| … |]
[e| … |]
または[| … |]
[| … |]
式の場合(∷ Q Exp
) -
[d| … |]
[d| … |]
アナウンス(∷ Q [Dec]
) -
[t| … |]
[t| … |]
型の場合(∷ Q Type
) -
[p| … |]
[p| … |]
パターン用(∷ Q Pat
)
したがって、括弧内には構文的に正しい式/宣言/型/パターンが必要です。
たとえば、引用
[| λ _ → 0 |]
(return $ LamE [WildP] (LitE (IntegerL 0)))
[| λ _ → 0 |]
は構造体です
(return $ LamE [WildP] (LitE (IntegerL 0)))
。 引用は(
Q Exp
だけでなく)
Q Exp
型であるため、引用モナド内で計算する必要があります。これにより、Template Haskellは、引用内に表示されるすべての識別子を
newName
生成された一意の識別子に置き換えることができます。 たとえば、引用符
[| λx → x |]
[| λx → x |]
は、次のコードに変換されます。
do id ← newName "x" return $ LamE [VarP id] (VarE id)
さらに、引用符で囲まれた括弧内で、接着(スプライシング)を使用できるため、THは、明示的に記述されたコードの一部と生成されたコードの一部を処理するマクロプリプロセッサとして機能することがわかります。 たとえば、引用符
[| 1 + $(fx) |]
[| 1 + $(fx) |]
(fx)
[| 1 + $(fx) |]
計算します。これは
Q Exp
型、式(
Exp
型の構造
(fx)
である必要があり、結果は通常のHaskellコードの形式で表示され、
$(fx)
代わりに置換( 貼り付け )されます、そして引用を続けます-結果のコードをそれを表す構造に変換します。 自動名前変更(実際には、このため、すべてが
Q
モナド内で行われます)のおかげで、引用内の同じコードの異なる挿入間でローカル変数名の競合はありません。 次の定義は、これをよく示しています。
summ ∷ Int → Q Exp summ n = summ' n [| 0 |] summ' ∷ Int → Q Exp → Q Exp summ' 0 code = code summ' n code = [| λx → $(summ' (n-1) [| $code + x |]) |]
このテンプレートは、
n
パラメーターを合計したラムダ式を生成します。 たとえば、
$(summ 3)
(λx1 → λx2 → λx3 → 0 + x1 + x2 + x3)
変換されます。 生成されたコードは、ネストされたラムダ式の引数に異なる識別子を使用しますが、テンプレート名は同じです。
[| λx → … |]
[| λx → … |]
。 この例に見られるように、 引用符とラベルのネストは任意ですが、それらを交互に並べることが重要です。引用内で引用してペースト内に貼り付けることはできません。
このような「準引用」は、Haskellプログラムを表す便利な方法です。 また、いくつかの制限があります。変数が出現するたびに、テンプレートが展開されるまで使用可能なスコープ内の値に関連付けられます。 このルールには3つのケースがあります。
- 引用符は、ある引用で使用されるローカル変数の別の引用からの「キャプチャ」を禁止します(通常のHaskellプログラムのように、その外側でクロージャー変数を使用することはできません)。 これは、前述の識別子の自動統合によるものです。 引用のみ
[p| … |]
[p| … |]
生成されたパターンが入力する変数の名前は変更[p| … |]
ません(これらの変数はパターンに関連付けられ、新しい任意の名前がある場合、それらにアクセスする方法は明確ではないため) 。 - 引用で使用されるグローバル識別子は、引用が定義されている環境で使用可能なすべての必要な識別子を「キャプチャ」します(これも通常のHaskellの場合と同様)。したがって、これらのすべての内部定義にアクセスできない他のモジュールでは、引用の値を問題なく使用できますまたは、同じ識別子に対して独自の定義があります。 このルールは、他のモジュールからのシンボル参照に内部GHCメカニズムを使用します(つまり、修飾します) 。 たとえば、引用符
[| map |]
[| map |]
は「GHC.Base.map
」に変換され、[t| [String] → Bool |]
[t| [String] → Bool |]
、「[GHC.Base.String] → GHC.Bool.Bool
」に変換されます。 テンプレートを貼り付けるスコープの識別子が必要な場合は、$(dyn "…")
ラッパーを使用します。 この方法では、他の人がローカルで定義した識別子を誤って使用する可能性があり、競合が発生するか、テンプレートが予期されるコードを生成しないため、dyn
のドキュメントにはこれが衛生的な機能ではないことが記載されています。 - また、引用符内では、ローカル関数変数を使用できます。 コンパイル段階では、これらは変数(単なる関連識別子)ですが、実行時にはこれらは単なる定数であるため、THは代わりに対応する値を単純に置き換えます。 そのため、式
let x = 5 in [| … x … |]
let x = 5 in [| … x … |]
はlet x = 5 in [| … $(lift x) … |]
変換されますlet x = 5 in [| … $(lift x) … |]
- つまり、Q Exp
typeのローカル変数の識別子を手動でラップする必要はありません 。
貼り付けと引用は相互に逆の操作です。1つは
Exp
構造をHaskellコードに変換し、もう1つはHaskellコードを
Exp
構造に変換するため、相互に消滅します。
$( [| |] ) ≡ [| $( ) |] ≡
これにより、THプログラムの開発中に生成されたHaskellコードの観点からのみ考えることができ、構文を表す内部構造について考えることはできません。
たとえば、「
$(summ 3)
」
$(summ 3)
の計算を検討してください。 テンプレートの使用をその定義に置き換えるだけです:
$(summ 3) $(summ' 3 [| 0 |]) $([| λx → $(summ' (3-1) [| $([| 0 |]) + x |]) |])
これで、余分な角かっこ
$([| … |])
を削除し、途中で「
x
」を一意の識別子に置き換えます。
λx1 → $(summ' (3-1) [| 0 + x1 |])
summ'
の定義を再び
summ'
ます。
λx1 → $([| λx → $(summ' (2-1) [| $([| 0 + x1 |]) + x |]) |])
次に、可能になるまで最後の2つの手順を繰り返します。
λx1 → λx2 → $( summ' (2-1) [| 0 + x1 + x2 |] ) λx1 → λx2 → $([| λx → $(summ' (1-1) [| $([| 0 + x1 + x2 |]) + x |]) |]) λx1 → λx2 → λx3 → $(summ' (1-1) [| 0 + x1 + x2 + x3 |]) λx1 → λx2 → λx3 → $( [| 0 + x1 + x2 + x3 |]) λx1 → λx2 → λx3 → 0 + x1 + x2 + x3
興味深いことに、この定義では、ラムダ式の左側(
λx1 → λx2 → …
)が再帰のパスに沿って再帰的に構築され、右側(
0 + x1 + …
)がテンプレートの残りの部分に同時に蓄積されます。
printf
テンプレートの例でも同じ手法が使用されています。
例:printf
次に、記事の最初の部分で言及した
printf
テンプレートの定義を調べます。 以下は、説明付きのコードと、それを使用するメインモジュールです。 コマンド
ghc -XTemplateHaskell --make Main.hs
でき
ghc -XTemplateHaskell --make Main.hs
- Main.hs
{-# LANGUAGE TemplateHaskell #-} module Main where -- printf import Printf (printf) -- $( … ) -- Haskell- -- – putStrLn main = putStrLn ( $(printf "Error in file %s line %d: %s") "io.cpp" 325 "printer not found" )
Printf.hs
{-# LANGUAGE TemplateHaskell #-} module Printf where -- Template Haskell import Language.Haskell.TH -- data Format = D -- "%d" – | S -- "%s" – | L String -- (L Literally) -- – Format parse :: String -> String -> [Format] parse "" rest = [L rest] parse ('%':'d':xs) rest = L rest : D : parse xs "" parse ('%':'s':xs) rest = L rest : S : parse xs "" parse (x:xs) rest = parse xs (rest++[x]) -- Haskell-, -- - gen :: [Format] -> ExpQ -> ExpQ gen [] code = code gen (D : xs) code = [| \x -> $(gen xs [| $code ++ show x |]) |] gen (S : xs) code = [| \x -> $(gen xs [| $code ++ x |]) |] gen (L s : xs) code = gen xs [| $code ++ s |] -- , -- printf :: String -> ExpQ printf s = gen (parse s "") [| "" |]
これは翻訳されたコメント付きの著者コードです。 同様の、しかしより短い(そして私の意見ではより単純な)解決策がここGistにあります。
その他の例
関数宣言の引用と名前の貼り付けを少し試しました。これらの実験についてはブログで説明しています。 アドバイスや推奨事項を喜んでいます。