7.16. Template Haskell

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.16.2. Template Haskellを使う)では、入門用の例として、論文の最初の例を使っている。

この文書は、GHCにおいてTemplate Haskellがどういう形で実現されているかを述べるものであり、Template Haskellを理解できるほど詳細ではない。Wikiページを見よ。

7.16.1. 構文

Template Haskellには以下に述べる新しい構文要素がある。これらの構文的拡張を有効にするにはフラグ-XTemplateHaskellを使う必要がある。

  • 接合は、$x(「x」は識別子)または$(...)(「...」は任意の式)と書く。「$」と識別子や括弧の間には空白があってはならない。「$」のこの使いかたは中置演算子としての意味より優先される。「MM.x」が中置演算子としての「.」よりも優先されるのと同じである。中置演算子が必要なら、周りにスペースを置くこと。

    接合は次の場所に現れ得る。

    • 式。接合される式の型はQ Expでなければならない。

    • パターン。接合される式の型はQ Patでなければならない。

    • 型。接合される式の型はQ Typeでなければならない。

    • 宣言のリスト。接合される式の型はQ [Dec]でなければならない。

    パターン接合には対応していないことに注意。接合の中ではインポートしたモジュールで定義された関数しか呼ぶことができず、同じモジュールの別の場所で定義された関数は呼ぶことが出来ない。

  • 式クォートは次のようにオックスフォード角括弧で囲んで書かれる。

    • [| ... |]または[e| ... |]、ただし「...」は式。このクォートの型はQ Expである。

    • [d| ... |]、ただし「...」は最上位の宣言の列。このクォートの型はQ [Dec]である。

    • [t| ... |]、ただし「...」は型。このクォートの型はQ Typeである。

    • [p| ... |]、ただし「...」はパターン。このクォートの型はQ Patである。

  • 型付きの式接合は$$x(xは識別子)または$$(...)(「...」は任意の式)と書く。

    型付きの式接合は式の代わりとしてのみ使うことができる。接合される式はQ (TExp a)という型を持たねばならない。

  • 型付きの式クォートは[|| ... ||]または[e|| ... ||]と書く。ここで「...」は式である。式「...」の型がaなら、このクォートの型はQ (TExp a)である。

    TExp a型の値は、関数unType :: TExp a -> Expを使うことでExp型の値に変換できる。

  • 準クォートはパターン文脈または式文脈に出現可能で、これもオックスフォード括弧を使って書く。

  • 名前は、一つか二つの一重引用符を前置することで、クォートすることができる。

    • 'fの型はNameであり、関数fを指す。同様に'Cの型はNameであり、データ構築子Cを指す。一般に、'thingthingを式文脈の中で解釈する。

      二文字目が一重引用符である名前は、(残念なことに)この方法でクォートすることができない。例えば、f'7という名前(これは合法なHaskellの識別子である)の関数があったとして、それをクォートしようとして'f'7と書くと、文字リテラル'f'の後に数値リテラル7が続いたものとしてパースされる。この(珍しい)状況の回避手段は今のところない。

    • ''Tの型はNameであり、型構築子Tを指す。つまり、''thingthingを型文脈の中で解釈する。

    これらのNameは、Template Haskellの式やパターンや宣言などを構築するのに使うことができる。また、関数reifyへ引数として与えることもできる。

  • 最上位の宣言接合においては、$(...)を省略してもよい。単に(宣言でなく)式を書けば、それが接合を意味する。例えば、以下のように書くことができる。

    module Foo where
    import Bar
    
    f x = x
    
    $(deriveStuff 'f)   -- $(...)記法を使う
    
    g y = y+1
    
    deriveStuff 'g      -- $(...)を省略する
    
    h z = z-1

    この省略によって、最上位の宣言接合がうるさくなくなり、怖い印象を与えないで済む。

  • 束縛は字句的なスコープを持つ。例として、下のコードを考える。ここで、gというBool -> Q Pat型の値が、別モジュールからインポートされてスコープにあるとする。

    y :: Int
    y = 7
    
    f :: Int -> Int -> Int
    f n = \ $(g True) -> y+n

    fの右辺のyは、最上位のy = 7を参照する。これは、パターン接合$(g n)yを束縛するものを生成する場合でも変わらない。

    パターン準クォート子は定義の右辺で可視であるような束縛を生成し得ることに注意。例えば、下のコードにおいて、準クォート子haskellがHaskellをパースするとすると、fの右辺にあるyは、最上位のy = 7ではなく、haskell準クォート子によって束縛されたyを参照する。

    y :: Int
    y = 7
    
    f :: Int -> Int -> Int
    f n = \ [haskell|y|] -> y+n
  • reifyが見ることができる型環境は、直前の宣言グループの終わりよりも上の最上位の宣言を含むが、それより下のものは含まない。

    宣言グループとは、ある最上位の宣言接合と、それ以降、次の宣言接合(これは含まない)までの全ての最上位の宣言を含むグループである。モジュールの最初の宣言グループは、最初の宣言接合までにある最上位の宣言全てからなる。

    具体的に、次のコードを考えよ。

    module M where
       import ...
       f x = x
       $(th1 4)
       h y = k y y $(blah1)
       $(th2 10)
       w z = $(blah2)

    この例において、

    1. 接合$(th1 ..)の中でのreifyは、fの定義を見ることができる。

    2. 接合$(blah)の中でのreifyは、fの定義を見ることができるが、hの定義を見ることはできない。

    3. 接合$(th2 ..)の中でのreifyは、fの定義と、$(th1..)によって作られた全ての束縛と、hの定義を見ることができる。

    4. 接合$(blah2)の中でのreifyは、$(th2...)と同じ定義群を見ることができる。

(元の論文と比較すると、細部に多くの違いがある。宣言接合では「splice」でなく「$」が使われる。中身の式の型は[Q Dec]でなくQ [Dec]でなければならない。型付きの式接合とクォートがサポートされている)

7.16.2. Template Haskellを使う

  • 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、ファイル毎)でも動作する。過去には前者二つについて制限があったが、これは撤廃された。

7.16.3. Template Haskellの実例

自信障壁(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

7.16.4. Template Haskellをプロファイルと併用する

Template Haskellは、接合する式を走らせるのに、GHCの組込みのバイトコードコンパイラと解釈実行器に依存している。バイトコードの解釈実行器は、コンパイルされた式を実行する際、GHC自身が土台としているのと同じランタイムを使う。従って、解釈される式から参照されるコンパイル済みコードは、このランタイムと互換性がなければならない。特に、プロファイルのためにコンパイルされたオブジェクトコードを接合式からロードして実行することはできない。プロファイル版のオブジェクトコードはプロファイル版のランタイムとしか互換性がないからである。

Template Haskellのコードを含む、複数のモジュールからなるプログラムがあって、それをプロファイル用にコンパイルする必要があるとき、このことが問題になる。接合を実行するときにプロファイル用オブジェクトコードをロードして用いることができないからである。幸運にも、GHCは回避策を提供している。基本的な考え方は、プログラムを二回コンパイルするというものである。

  1. -profなしで、通常の方法でそのプログラムまたはライブラリをコンパイルする。

  2. -pdof付きで、さらにオブジェクトファイルの名前を別のものにするために-osuf p_o(通常の接尾辞以外なら何を使っても良い)も付けて、もう一度コンパイルする。GHCは、接合式を実行する際、最初の手順で構築されたオブジェクトファイルを自動的にロードする。-prof付きでコンパイルするとき、-osufを使わず、かつTemplate Haskellが使われているなら、GHCはエラーメッセージを出力する。

7.16.5. Template Haskellの準クォート

準クォートは、パターンおよび式をプログラマが定義した具象構文を使って書くことを可能にする。この拡張の背後にある動機といくつかの例が"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と同じステージ制限に従わねばならない。たとえば、この例においてexprMain.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