テンプレートHaskellの紹介。 パート2.コード引用ツール

このテキストは、Bulat Ziganshinによって書かれたTemplate Haskellドキュメントの翻訳です。 テキスト全体の翻訳は、知覚を容易にするためにいくつかの論理部分に分割されます。 テキスト内のその他の斜体は、翻訳者のメモです。 その他の部品:





引用モナド



テンプレートは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つのタイプがあります。



したがって、括弧内には構文的に正しい式/宣言/型/パターンが必要です。

たとえば、引用[| λ _ → 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つのケースがあります。

  1. 引用符は、ある引用で使用されるローカル変数の別の引用からの「キャプチャ」を禁止します(通常のHaskellプログラムのように、その外側でクロージャー変数を使用することはできません)。 これは、前述の識別子の自動統合によるものです。 引用のみ[p| … |]



    [p| … |]



    生成されたパターンが入力する変数の名前は変更[p| … |]



    ません(これらの変数はパターンに関連付けられ、新しい任意の名前がある場合、それらにアクセスする方法は明確ではないため)
  2. 引用で使用されるグローバル識別子は、引用が定義されている環境で使用可能なすべての必要な識別子を「キャプチャ」します(これも通常のHaskellの場合と同様)。したがって、これらのすべての内部定義にアクセスできない他のモジュールでは、引用の値を問題なく使用できますまたは、同じ識別子に対して独自の定義があります。 このルールは、他のモジュールからのシンボル参照に内部GHCメカニズムを使用します(つまり、修飾します) 。 たとえば、引用符[| map |]



    [| map |]



    は「 GHC.Base.map



    」に変換され、 [t| [String] → Bool |]



    [t| [String] → Bool |]



    、「 [GHC.Base.String] → GHC.Bool.Bool



    」に変換されます。 テンプレートを貼り付けるスコープの識別子が必要な場合は、 $(dyn "…")



    ラッパーを使用します。 この方法では、他の人がローカルで定義した識別子を誤って使用する可能性があり、競合が発生するか、テンプレートが予期されるコードを生成しないため、 dyn



    のドキュメントにはこれが衛生的な機能ではないことが記載されています。
  3. また、引用符内では、ローカル関数変数を使用できます。 コンパイル段階では、これらは変数(単なる関連識別子)ですが、実行時にはこれらは単なる定数であるため、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








All Articles