Template HaskellはHaskellでコンパイル時メタプログラミングをすることを可能にする。主要な技術的革新の背景は"Template Meta-programming for Haskell" (Proc Haskell Workshop 2002)で議論されている。
Template Haskellに関するwikiページがhttp://www.haskell.org/haskellwiki/Template_Haskell/にある。また、オンラインHaskellライブラリリファレンスを参照することもできる(Language.Haskell.TH
というモジュールを探すと良い)。元の設計からの多くの変更点がNotes on Template Haskell version 2に記されている。しかし、これらの変更点が全てGHCにある訳ではない。
下(7.12.2. Template Haskellを使う)では、入門用の例として、論文の最初の例を使っている。
この文書は、GHCにおいてTemplate Haskellがどういう形で実現されているかを述べるものであり、Template Haskellを理解できるほど詳細ではない。Wikiページを見よ。
Template Haskellには以下に述べる新しい構文要素がある。これらの構文的拡張を有効にするにはフラグ-XTemplateHaskell
を使う必要がある。(-XTemplateHaskell
は、-fglasgow-exts
を使っていても、もはや自動的に有効にならない)
接合は、$x
(「x
」は識別子)または$(...)
(「...」は任意の式)と書く。「$」と識別子や括弧の間には空白があってはならない。「$」のこの使いかたは中置演算子としての意味より優先される。「MM.x」が中置演算子としての「.」よりも優先されるのと同じである。中置演算子が必要なら、周りにスペースを置くこと。
接合は次の場所に現れ得る。
式。接合される式の型はQ Exp
でなければならない。
型。接合される式の型はQ Typ
でなければならない。
トップレベル宣言のリスト。接合される式の型はQ [Dec]
でなければならない。
パターン接合には対応していないことに注意。接合の中ではインポートしたモジュールで定義された関数しか呼ぶことができず、同じモジュールの別の場所で定義された関数は呼ぶことが出来ない。
式クォートは次のようにオックスフォード角括弧で囲んで書かれる。
[| ... |]
または[e| ... |]
、ただし「...」は式。このクォートの型はQ Exp
である。
[d| ... |]
、ただし「...」は最上位の宣言の列。このクォートの型はQ [Dec]
である。
[t| ... |]
、ただし「...」は型。このクォートの型はQ Type
である。
[p| ... |]
、ただし「...」はパターン。このクォートの型はQ Pat
である。
準クォートはパターン文脈または式文脈に出現可能で、これもオックスフォード括弧を使って書く。
[
、
ただし「...」は任意の文字列である。準クォート機能の完全な記述は7.12.5. Template Haskellの準クォートにある。varid
| ... |]
名前は、一つか二つの一重引用符を前置することで、クォートすることができる。
'f
の型はName
であり、関数f
を指す。同様に'C
の型はName
であり、データ構築子C
を指す。一般に、'
thing
はthing
を式文脈の中で解釈する。
''T
の型はName
であり、型構築子T
を指す。つまり、''
thing
はthing
を型文脈の中で解釈する。
これらのName
は、Template Haskellの式やパターンや宣言などを構築するのに使うことができる。また、関数reify
へ引数として与えることもできる。
最上位の宣言接合においては、$(...)
を省略してもよい。単に(宣言でなく)式を書けば、それが接合を意味する。例えば、以下のように書くことができる。
module Foo where import Bar f x = x $(deriveStuff 'f) -- $(...)記法を使う g y = y+1 deriveStuff 'g -- $(...)を省略する h z = z-1
この省略によって、最上位の宣言接合がうるさくなくなり、怖い印象を与えないで済む。
splice
」でなく「$
」が使われる。中身の式の型は[Q Dec]
でなくQ [Dec]
でなければならない。パターン接合とクォーテーションは実装されていない。)
Template Haskell用のデータ型とモナド構築関数はライブラリLanguage.Haskell.THSyntax
にある。
ある関数をコンパイル時に走らせることができるのは、その関数が別のモジュールからインポートされているときだけである。言い替えると、あるモジュールで関数を定義し、同じモジュールの接合でその関数を呼ぶということはできない。(そうすることに意味がない訳ではない。ただ実装が難しい)
ある関数をコンパイル時に走らせることができるのは、その関数のインポート元モジュールが、コンパイル中のモジュールと同じ相互再帰グループに属していない場合だけである。さらに、その相互再帰グループの全てのモジュールが、接合が実行されるモジュールから非SOURCEなインポートを介して到達可能でなければならない。
例として、Aというモジュールをコンパイルしているとき、BからインポートされたTemplate Haskellの関数を走らせることができるのは、BがAを(直接または間接に)インポートしていない場合だけである。理由は明白なはずである。Bを走らせるにはAをコンパイルして走らせねばならないが、我々は今まさにAを型検査しているのだから。
-ddump-splices
フラグを使うと、最上位の接合が展開されるたびにそれが表示される。
GHCをソースからビルドしているなら、Template Haskellを使うには少なくともstage-2のブートストラップコンパイラが必要である。stage-1コンパイラはTHの要素を受け付けない。これは次のような理由による。THコンパイラはプログラムを実行し、その結果を見る。そのため、コンパイルされるプログラムの出力がコンパイラのものと同じ表現を持っていることが重要である。
Template Haskellはどのモード(--make
、--interactive
、ファイル毎)でも動作する。過去には前者二つについて制限があったが、これは撤廃された。
自信障壁(confidence barrier)を乗り越えるために、以下の、枠組を理解してもらうための例を試してみてほしい。まず、下の二つのモジュールを「Main.hs」と「Printf.hs」とにコピペする。
{- Main.hs -} module Main where -- 後で定義する「pr」というテンプレートをインポートする。 import Printf ( pr ) -- 接合演算子$は、prによってコンパイル時に生成された -- Haskellのソースコードをとり、これをputStrLnの引数とし -- て接合する。 main = putStrLn ( $(pr "Hello") ) {- Printf.hs -} module Printf where -- printfの枠組。論文より。 -- 使うモジュールとは別の場所で定義されていなければなら -- ない。 -- Template Haskellの構文をインポートする import Language.Haskell.TH -- 書式文字列を記述する data Format = D | S | L String -- 書式文字列をパースする。ここでの目的は我々の最初の -- Template Haskellのプログラムを組み上げることであり、 -- printfを組むことではないので、大部分未実装のまま残し -- てある。 parse :: String -> [Format] parse s = [ L s ] -- パースされた書式文字列の表現からHaskellのソースコー -- ドを生成する。生成されたコードは、「pr」を呼んだモジ -- ュールにコンパイル時に接合される。 gen :: [Format] -> Q Exp gen [D] = [| \n -> show n |] gen [S] = [| \s -> s |] gen [L s] = stringE s -- 入力の書式文字列から、接合するHaskellコードを生成する。 pr :: String -> Q Exp pr s = gen (parse s)
次にコンパイラを走らせる。(これはWindows上のCygwinプロンプトである)
$ ghc --make -XTemplateHaskell main.hs -o main.exe
「main.exe」を走らせれば出力が得られる。
$ ./main Hello
Template Haskellは、接合する式を走らせるのに、GHCの組込みのバイトコードコンパイラと解釈実行器に依存している。バイトコードの解釈実行器は、コンパイルされた式を実行する際、GHC自身が土台としているのと同じランタイムを使う。従って、解釈される式から参照されるコンパイル済みコードは、このランタイムと互換性がなければならない。特に、プロファイルのためにコンパイルされたオブジェクトコードを接合式からロードして実行することはできない。プロファイル版のオブジェクトコードはプロファイル版のランタイムとしか互換性がないからである。
Template Haskellのコードを含む、複数のモジュールからなるプログラムがあって、それをプロファイル用にコンパイルする必要があるとき、このことが問題になる。接合を実行するときにプロファイル用オブジェクトコードをロードして用いることができないからである。幸運にも、GHCは回避策を提供している。基本的な考え方は、プログラムを二回コンパイルするというものである。
-prof
なしで、通常の方法でそのプログラムまたはライブラリをコンパイルする。
-pdof
付きで、さらにオブジェクトファイルの名前を別のものにするために-osuf p_o
(通常の接尾辞以外なら何を使っても良い)も付けて、もう一度コンパイルする。GHCは、接合式を実行する際、最初の手順で構築されたオブジェクトファイルを自動的にロードする。-prof
付きでコンパイルするとき、-osuf
を使わず、かつTemplate Haskellが使われているなら、GHCはエラーメッセージを出力する。
準クォートは、パターンおよび式をプログラマが定義した具象構文を使って書くことを可能にする。この拡張の背後にある動機といくつかの例が"Why It's Nice to be Quoted: Quasiquoting for Haskell" (Proc Haskell Workshop 2007)に述べられている。以下の例では、単純な式言語のための準クォータをどうやって書くかを紹介する。
目立つ特徴を挙げる。
準クォートは[
の形をとる。
quoter
| string
|]
quoter
は、インポートされたクォート子(quoter)の(修飾なしの)名前でなければならない。任意の式を置ける訳ではない。
quoter
は、"e
"、"t
"、"d
"、 "p
"であってはならない。これらはTemplate Haskellのクォートと重なるからである。
トークン[
の間にスペースがあってはいけない。
quoter
|
クォートされるstring
は任意であり、改行を含んでもよい。
クォートされるstring
は、"|]"
という二文字の並びが最初に出現したところで終わる。一切のエスケープは行なわれない。文字列中にこの文字の並びを埋め込みたい場合、自分でエスケープ方式を発明し(たとえば"|~]"
で代用するなど)、クォート関数が"|~]"
を"|]"
と解釈するようにしなければならない。これをする一つの方法は、あなたのエスケープ方式を実行する前処理過程をクォート子に合成することである。詳細はTrac上の議論を見よ。
準クォートは以下のものが置ける場所に出現できる。
式
パターン
型
最上位の宣言
(論文には最初の二つだけが記述されている)
クォート子はLanguage.Haskell.TH.Quote.QuasiQuoter
という型の値である。これは次のように定義されている。
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
すなわち、クォート子は四つのパーサからなるタプルである。準クォートが出現できる文脈それぞれに対して一つずつである。
準クォートを展開する際は、オックスフォード角括弧で囲まれた文字列に、適切なパーサを適用することで行なう。その準クォートの置かれた文脈(式、パターン、型、宣言)によって、どのパーサが呼ばれるかが決まる。
下の例は、準クォートを実際に使っているところを示すものである。クォート子expr
は、Expr
モジュールで定義されており、QuasiQuoter
型の値に束縛されている。この例では、'int:n
という構文を使って、変数n
を逆クォートしたものを表している(逆クォートにこの構文を使うことはこのパーサの作者が決めたことであり、GHCによって決められたことではない)。パターン照合の際、これはn
を構築子IntExpr
の整数引数に束縛する。逆クォートに関する更なる詳細については参照した論文を見てほしい。この論文にはString -> a
という型の単一のパーサを活用して、Q Exp
型の値を返す式パーサと、Q Pat
型の値を返すパターンパーサの両方を生成するためのSYBを使ったテクニックの説明もある。
準クォータはTemplate Haskellと同じステージ制限に従わねばならない。たとえば、この例においてexpr
はMain.hs
で使われているので、そこで定義されることはできず、インポートされねばならない。
{- ------------- Main.hsというファイル --------------- -} module Main where import Expr main :: IO () main = do { print $ eval [expr|1 + 2|] ; case IntExpr 1 of { [expr|'int:n|] -> print n ; _ -> return () } } {- ------------- Expr.hsというファイル --------------- -} module Expr where import qualified Language.Haskell.TH as TH import Language.Haskell.TH.Quote data Expr = IntExpr Integer | AntiIntExpr String | BinopExpr BinOp Expr Expr | AntiExpr String deriving(Show, Typeable, Data) data BinOp = AddOp | SubOp | MulOp | DivOp deriving(Show, Typeable, Data) eval :: Expr -> Integer eval (IntExpr n) = n eval (BinopExpr op x y) = (opToFun op) (eval x) (eval y) where opToFun AddOp = (+) opToFun SubOp = (-) opToFun MulOp = (*) opToFun DivOp = div expr = QuasiQuoter { quoteExp = parseExprExp, quotePat = parseExprPat } -- Exprをパースし、その表現をQ ExpまたはQ Patとして返す。 -- SYBを使うことによって、二つのパーサを別々に書くことなく -- 一つのString -> Exprという型のパーサを書くだけで済ます -- 方法については、参照先の論文を見よ。 parseExprExp :: String -> Q Exp parseExprExp ... parseExprPat :: String -> Q Pat parseExprPat ...
ではコンパイラを走らせよう。
$ ghc --make -XQuasiQuotes Main.hs -o main
"main"を走らせれば次のような出力になる。
$ ./main 3 1