Haskell Language
テンプレートHaskell&QuasiQuotes
サーチ…
備考
テンプレートハスケルとは何ですか?
テンプレートHaskellは、GHC Haskellに組み込まれているテンプレートのメタプログラミング機能を指します。元の実装を記述した論文は、 ここで見つけることができます 。
ステージとは何ですか? (または、ステージ制限は何ですか?)
ステージとは、コードが実行されるときを指します。通常、コードは実行時にのみ展開されますが、テンプレートHaskellではコンパイル時にコードを実行できます。 「標準」コードはステージ0で、コンパイル時コードはステージ1です。
ステージ制限とは、ステージ0のプログラムがステージ1で実行されないということです。これは、コンパイル時に(メタプログラムだけでなく) 通常のプログラムを実行できることと同等です。
規約(および実装の単純化のため)では、現在のモジュール内のコードは常にステージ0であり、他のすべてのモジュールからインポートされたコードはステージ1です。このため、他のモジュールからの式だけをスプライスできます。
ステージ1のプログラムはQ Exp
、 Q Type
などのステージ0式であることに注意してください。逆は真ではありません。タイプQ Exp
すべての値(ステージ0プログラム)がステージ1プログラムではなく、
さらに、スプライスは入れ子にすることができるので、識別子は1より大きいステージを持つことができます。ステージ制限は一般化できます。ステージnプログラムはm> nのどのステージでも実行できません。たとえば、特定のエラーメッセージで、1より大きいステージへの参照を参照できます。
>:t [| \x -> $x |]
<interactive>:1:10: error:
* Stage error: `x' is bound at stage 2 but used at stage 1
* In the untyped splice: $x
In the Template Haskell quotation [| \ x -> $x |]
テンプレートを使うHaskellは無関係の識別子からスコープ内のエラーを引き起こしますか?
通常、単一のHaskellモジュール内のすべての宣言は、すべてが相互に再帰的であるとみなすことができます。言い換えれば、すべてのトップレベルの宣言は、単一のモジュール内の他のすべての宣言の範囲内にあります。 Template Haskellを有効にすると、有効範囲規則が変更されます。代わりに、モジュールはTHスプライスで区切られたコードのグループに分割され、各グループは相互に再帰的であり、各グループはすべてのグループのスコープ内にあります。
Qタイプ
Language.Haskell.TH.Syntax
で定義されたQ :: * -> *
型のコンストラクタは、計算が実行されるモジュールのコンパイル時の環境にアクセスできる抽象型です。 Q
型はまた、THによる名前の取り込みと呼ばれる変数の置換を処理します( ここで説明します)。すべてのスプライスは、一部のX
QX
型を持ちます。
コンパイル時の環境は次のとおりです。
- スコープ内の識別子および前記識別子に関する情報を含み、
- 関数の種類
- コンストラクタの型とソースデータ型
- 型宣言(クラス、型族)の完全指定
- スプライスが発生するソースコード内の場所(行、列、モジュール、パッケージ)
- 機能の固定(GHC 7.10)
- 有効なGHC拡張(GHC 8.0)
Q
型には、関数newName :: String -> Q Name
使って新しい名前を生成する機能もあります。名前は暗黙のうちに束縛されていないので、ユーザーはそれ自身をバインドしなければならないので、名前の使用結果が有効範囲に入っていることを確認することはユーザーの責任です。
Q
はFunctor,Monad,Applicative
インスタンスがあり、これはLanguage.Haskell.TH.Lib
で提供されているコンビネータとともに、 Q
値を操作するためのメインインターフェイスです。フォームのTH astのすべてのコンストラクタのヘルパ関数を定義します:
LitE :: Lit -> Exp
litE :: Lit -> ExpQ
AppE :: Exp -> Exp -> Exp
appE :: ExpQ -> ExpQ -> ExpQ
ExpQ
、 TypeQ
、 DecsQ
、およびPatQ
は、通常Q
タイプ内に格納されるASTタイプの同義語であることに注意してください。
THライブラリは、機能を提供runQ :: Quasi m => Q a -> ma
、そしてインスタンスがQuasi IO
それがあることと思われるので、 Q
タイプはちょうど空想であるIO
。しかし、使用runQ :: Q a -> IO a
生産IO
任意のコンパイル時の環境へのアクセス権を持っていないアクションを-これは実際にのみ使用可能ですQ
タイプ。そのようなIO
アクションは、前記環境にアクセスしようとすると実行時に失敗する。
n-arityカレー
おなじみの
curry :: ((a,b) -> c) -> a -> b -> c
curry = \f a b -> f (a,b)
関数は、任意のアリティのタプルに一般化することができます。例:
curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d
curry4 :: ((a, b, c, d) -> e) -> a -> b -> c -> d -> e
しかし、手作業で2のタプル(例えば)20のタプルの関数を書くことは面倒です(プログラム内の20タプルの存在がレコードで修正されるべき設計上の問題をほぼ確実に示しているという事実は無視してください)。
テンプレートHaskellを使って、任意のn
に対してこのようなcurryN
関数を生成することができます:
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad (replicateM)
import Language.Haskell.TH (ExpQ, newName, Exp(..), Pat(..))
import Numeric.Natural (Natural)
curryN :: Natural -> Q Exp
curryN
関数は自然数を取り、そのcurryN
のカリー関数をHaskell ASTとして生成します。
curryN n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
最初に、関数の引数のそれぞれに新しい型の変数を生成します.1つは入力関数用、もう1つは関数の引数用です。
let args = map VarP (f:xs)
式args
はパターンf x1 x2 .. xn
表しf x1 x2 .. xn
。パターンは別個の構文エンティティであることに注意してください。この同じパターンをラムダ、関数バインディング、またはletバインディングのLHS(これはエラーになります)に配置することもできます。
ntup = TupE (map VarE xs)
この関数は、引数のシーケンスから引数タプルを構築する必要があります。これは、ここで行ったことです。パターン変数( VarP
)と式変数( VarE
)の区別に注意してください。
return $ LamE args (AppE (VarE f) ntup)
最後に、私たちが生成する値は、AST \f x1 x2 .. xn -> f (x1, x2, .. , xn)
です。
引用符と '持ち上げた'コンストラクタを使ってこの関数を書くこともできます:
...
import Language.Haskell.TH.Lib
curryN' :: Natural -> ExpQ
curryN' n = do
f <- newName "f"
xs <- replicateM (fromIntegral n) (newName "x")
lamE (map varP (f:xs))
[| $(varE f) $(tupE (map varE xs)) |]
引用は構文的に有効でなければならないので、 [| \ $(map varP (f:xs)) -> .. |]
通常のHaskellではパターンの 'リスト'を宣言する方法がないため、 \ var -> ..
[| \ $(map varP (f:xs)) -> .. |]
は無効[| \ $(map varP (f:xs)) -> .. |]
上記は\ var -> ..
と解釈され、スプライスされた式は、パターンのリストではなく、単一のパターンであるPatQ
型を持つと予想されます。
最後に、このTH関数をGHCiにロードすることができます。
>:set -XTemplateHaskell
>:t $(curryN 5)
$(curryN 5)
:: ((t1, t2, t3, t4, t5) -> t) -> t1 -> t2 -> t3 -> t4 -> t5 -> t
>$(curryN 5) (\(a,b,c,d,e) -> a+b+c+d+e) 1 2 3 4 5
15
テンプレートHaskellとQuasiquotesの構文
テンプレートHaskellは、 -XTemplateHaskell
GHC拡張によって有効になります。この拡張により、このセクションで詳しく説明されているすべての構文機能が使用可能になります。 Template Haskellの詳細は、 ユーザーガイドに記載されています 。
スプライス
スプライスは、
$(...)
と書かれたTemplate Haskellによって有効にされた新しい構文エンティティ$(...)
ここで、(...)
は式です。$
と式の最初の文字の間にスペースを入れてはいけません。そしてテンプレートHaskellは解析のオーバーライド$
例えば-演算子f$g
通常のように解析される($) fg
テンプレートHaskellのを有効にして、それはスプライスとして解析される一方。トップレベルにスプライスが現れると、
$
を省略することができます。この場合、スプライスされた式は行全体です。スプライスは、コンパイル時に実行されてHaskell ASTを生成するコードを表し、ASTはHaskellコードとしてコンパイルされ、プログラムに挿入されます
式、パターン、型、およびトップレベルの宣言の代わりにスプライスを表示できます。スプライスされた式のタイプは、それぞれそれぞれ
Q Exp
、Q Pat
、Q Type
、Q [Decl]
です。宣言スプライスはトップレベルにのみ表示され、他のスプライスは他の式、パターン、またはタイプの内側に表示されることに注意してください。
表現引用(注:準引用符ではない )
式引用は、次のいずれかの形式で記述された新しい構文エンティティです。
-
[e|..|]
または[|..|]
-..
は式であり、クォーテーションの型はQ Exp
です。 -
[p|..|]
-..
はパターンであり、クォートの型はQ Pat
です。 -
[t|..|]
-..
は型であり、引用符はQ Type
です。 -
[d|..|]
-..
は宣言のリストであり、引用の型はQ [Dec]
です。
-
式引用は、コンパイル時間プログラムを取り、そのプログラムによって表されるASTを生成する。
スプライスなしで引用符で値(例:
\x -> [| x |]
)を使用すると、\x -> [| $(lift x) |]
ここで、lift :: Lift t => t -> Q Exp
はクラスから来る
class Lift t where lift :: t -> Q Exp default lift :: Data t => t -> Q Exp
入力されたスプライスと引用
型付きスプライスは、前に述べた(型なし)スプライスと同様で、
$$(..)
と書かれてい$$(..)
ここで、(..)
は式です。e
がタイプQ (TExp a)
場合、$$e
はタイプa
ます。型付き引用符は、形式
[||..||]
取り[||..||]
ここで..
はa
型の式です。結果の見積はタイプQ (TExp a)
ます。型付き式は型なし式に変換できます:
unType :: TExp a -> Exp
。
準疑問
QuasiQuotesは式の引用を一般化しています - 以前は、式の引用で使用されるパーサーは固定セット(
e,p,t,d
)のいずれかですが、QuasiQuotesではカスタムパーサーを定義してコンパイル時にコードを生成できます。疑似引用は、すべての同じ文脈で、通常の引用と同様に現れることがあります。準引用符は
[iden|...|]
と書かれてい[iden|...|]
ここで、iden
はLanguage.Haskell.TH.Quote.QuasiQuoter
型の識別子です。QuasiQuoter
は、単純に4つのパーサーで構成されています。各パーサーは、引用が表示されるさまざまなコンテキストごとに1つずつあります。
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
名前
Haskellの識別子は、
Language.Haskell.TH.Syntax.Name
型で表されます。名前は、テンプレートHaskellのHaskellプログラムを表す抽象構文木の葉を形成します。現在スコープ内にある識別子は、
'e
または'T
いずれかの名前に変換することができます。最初のケースでは、e
は式スコープで解釈され、2番目のケースではT
は型スコープにあります(型と値のコンストラクタがHaskellでの共通性なしで名前を共有することを思い出してください)。