[Haskell] Quasi-quote bracketsを使ってTemplateを書く。

id:MaD:20070906ではTemplate Haskellのコードを書く際に直接Exp型のデータを操作しましたが、Quasi-quote bracketsを使うともっと簡単にテンプレートを書くことができます。

ghc, ghciを起動する際にはオプションとして-fthが必要であることに注意してください。

Quasi-quote bracketには以下の物があります。

Expression brackets

[| ... |]または[e| ... |]は式です。

>:m +Language.Haskell.TH
>runQ [e| 1 + 2 |]
InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2)))
>runQ [| 1 + 2 |]
InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2)))

データコンストラクタの末尾にはEがつきます。

Type brackets

[t| ... |]は型です。

>runQ [t| Int |]
ConT GHC.Base.Int
>runQ [t| String -> Int |]
AppT (AppT ArrowT (ConT GHC.Base.String)) (ConT GHC.Base.Int)

データコンストラクタの末尾にはTがつきます。

Declaration brackets

[d| ... |]は関数定義やクラス定義などの定義文です。

>runQ [d| main = putStrLn "Hello World" |]
[ValD (VarP main) (NormalB (AppE (VarE System.IO.putStrLn) (LitE (StringL "Hello World")))) []]
>runQ [d| type String = [Char] |]
[TySynD String [] (AppT ListT (ConT GHC.Base.Char))]

データコンストラクタの末尾にはDがつきます。

Pattern brackets

[p| ... |]はパターンマッチで使われるパターンです。

>runQ [p| (x, _) |]
TupP [VarP x_0, WildP]

みたいになるはずですが、GHC-6.6ではまだサポートされていないようです。

シングルクオート

', ''は変数名、型名をその時の文脈に従って適切なName型へと変換します。

>'map
GHC.Base.map
>''Int
GHC.Base.Int
>''Monad
GHC.Base.Monad

GHC.Base.の部分がインポートされているモジュールにより決まります。

サンプル

Quasi-quote Bracketsのみを使って(内部表現に触らずに)使うのが一番簡単なTemplate Haskellの使い方です。
例題として、ファイル入出力を記述する為のテンプレートを書いてみます。ここではあえてreadFile等は使わずハンドラを直接操作します。

ファイル入出力は

  1. ファイルをオープンしてハンドラを取得(openFile)
  2. ハンドラを使って色々する
  3. 忘れずにファイルをクローズ(hClose)

という流れになりますが、最初と最後は常に同じです。なのでこの共通する部分をテンプレートにし、ファイル名と真ん中の処理を外からはめ込めるようにします。

用意するファイルはまずOpen.hsです。

{-# OPTIONS_GHC -fth #-}
module Open where
import IO
import Language.Haskell.TH

open :: String -> ExpQ -> ExpQ
open filename exp = [| 
        do h <- openFile filename ReadMode
           $exp h
           hClose h |]

引数としてファイル名と、式をそのままもらってテンプレートの中に展開します($expの部分)。展開した式にハンドラhを渡しその後ハンドラをクローズしています。
普通に書いたHaskellのコードが、Quasi-quote bracketによってExpQ型へ変換されます。

呼び出し側は以下のようになります。ファイル名は文字列、メイン処理は[| ... |]で囲ってテンプレート化してopenに渡します。
これがコンパイルされる時に、openが実行されその戻り値であるExpQ型が返ってくるので、Splice記法( $(...) )によりコンパイル中のコード接合してからコードをコンパイルします。

{-# OPTIONS_GHC -fth #-}
import Open
import IO

main :: IO ()
main = $(open("Open.hs")
         [| \h ->             -- ファイルのハンドラを引数としてもらう
            do str <- hGetContents h
               putStrLn.reverse $ str   -- とりあえずファイルの中身を逆順にして表示
         |])
実行例
% ./open_test                                                                                                                       

]| h esolCh           
h pxe$           
edoMdaeR emanelif eliFnepo -< h od        
 |[ = pxe emanelif nepo
QpxE >- QpxE >- gnirtS :: nepo

HT.lleksaH.egaugnaL tropmi
OI tropmi
erehw nepO eludom
}-# htf- CHG_SNOITPO #-{

Liftクラス

まだ説明していないのですがLiftクラスという物があって、このクラスに属するデータは[| ... |]内の解析時に自動的にExpQ型に変換されます。上のコードで引数であるfilenameを直接[| ... |]内に書き込めるのはStringがLiftクラスのインスタンスだからです。

上のコードではReadModeをハードコーディングしていますが、これはIOMode型がLiftクラスのインスタンスではないので、ExpQ型に直接変換できないからです。

そこで、IOModeをLiftクラスのインスタンスにしてみようと思います。
手順は、まずrunQを利用してデータ表現を調査

> runQ [| ReadMode |]
ConE GHC.IOBase.ReadMode

base/GHC/IOBase.lhsをのぞいて、IOModeに何があるかを調査。ReadMode, WriteMode, AppendMode, ReadWriteModeがある。

libraries/template-haskell/Language/Haskell/TH/Syntax.hsをのぞいて、どうやって定義すればいいかを調査。データコンストラクタを定義する為にはmkNameG_dが使えるらしいです。これはデータコンストラクタ用の名前を"パッケージ名" "名前空間名" "データコンストラクタ名"から生成する関数のようです。

最終的なOpen.hsは以下のようになります。*1

{-# OPTIONS_GHC -fth #-}
module Open where
import IO
import Language.Haskell.TH
import Language.Haskell.TH.Syntax

instance Lift IOMode where
    lift ReadMode      = return.ConE $ mkNameG_d "base" "GHC.IOBase" "ReadMode"
    lift WriteMode     = return.ConE $ mkNameG_d "base" "GHC.IOBase" "WriteMode"
    lift AppendMode    = return.ConE $ mkNameG_d "base" "GHC.IOBase" "AppendMode"
    lift ReadWriteMode = return.ConE $ mkNameG_d "base" "GHC.IOBase" "ReadWriteMode"

open :: (String, IOMode) -> ExpQ -> ExpQ
open (filename, mode) exp = [| 
        do h <- openFile filename mode
           $exp h
           hClose h |]

呼び出し側。openの入れ子も大丈夫です。

{-# OPTIONS_GHC -fth #-}
import Open
import IO

main :: IO ()
main = do 
   $(open("Open.hs", ReadMode)
        [| \h -> do
            str <- hGetContents h 
            $(open("output.txt", WriteMode) [| \h -> hPutStrLn h str |])
        |])

まぁ、このくらいの用途だったら直接関数を渡す方がずっと良いと思います。せっかく高階関数が使えるうえに、Haskellは遅延評価なのですからわざわざ式をそのまま渡す必要がありません。
関数を直接渡す場合と異なるのは、例えば関数がインライン化されるとか式の内部をいじれるとかいったことがあります。基本的には高階関数などではどうしようもない問題に対して使うことになるんだと思います。

[d| .. |]などを使ったコードについては、また後日勉強して書いていきたいと思います。

Template Haskellでメタメタ...プログラミング

open関数ではTemplate Haskellのテンプレートそのものをexpからコンパイル時に生成しているわけで、実は結構難しいことをしてくれています。いわゆるメタメタプログラミングです。どのくらいの深さまでメタにできるかは分かりません。

こういうQuasi-quote bracketが入れ子になったものをコンパイルするために、内部的に、

  1. コンパイルと実行
  2. Quasi-quote bracket内の解析
  3. テンプレートのコード中への接合

という3つの状態をいったり来たりしながらコンパイルを行っています。参考文献

Template Haskellコンパイル時にファイル入出力等を行えるようになっているので、コンパイルそのものをプログラムの実行に見立てる事もできますね。Template HaskellHaskellを使って書かれていますから、既存のアプリケーションをコンパイル時に走らせて何かするのも簡単です。
*2

*1:最新のリポジトリを覗いたところ、ExpやTypeなどがGenerics.DataとTypableのインスタンスになっていたので、ここはもっと書きやすくなると思います。deriving(Lift)とかを考えているんじゃないかと思います。

*2:ただし、Template haskellではIO処理の安全性は保証されていません