Template Haskell入門

Template Haskell(以下TH)超入門です。
まあ入門するのは僕ですが。
Haskellの易しいエントリがあまり見あたらないため、僕が書いていくことにしようとか考えたり。

続き

よかったらこちらもどうぞ。

前提

GHC7.0.2で。
GHC7.0.1からQuasiQuotesの記法が一部変更したようです。


..と下書きを書いて既に数ヶ月が経とうとしています。
実はこの記事はHIMA' #6に参加したことをきっかけとして書いています。


7/24(Sun)にはスタートHaskellが開かれます!


....それも終わって既に2ヶ月経ってますね。やる気ないですね僕。

しかしHaskellは熱いですね!
そんなHaskellメタプログラミングの世界を覗いてみようと思います。

おまじない

ファイルに書く場合、ファイル先頭に以下を記述。

{-# LANGUAGE TemplateHaskell, QuasiQuotes #-}
import Language.Haskell.TH

ghciの場合。上と同じ設定にするために以下のコマンドを打ちます。

ghci> :set -XTemplateHaskell
ghci> :set -XQuasiQuotes
ghci> :module + Language.Haskell.TH

以下おまじない省略。

Template Haskellとは

Type-safe compile-time meta-programmingと説明されていますね。
Haskellにおけるマクロと考えれば良いようです。コンパイル時にHasellの構文木をいじることが出来ます。
ソースコード自体が構文木Lispほど簡単には書けませんが、代わりに型に守られているため、安全に書けることが保証されているようです。

Qモナド

Quoteモナドと解釈してよいのかな。
HaskellのコードをOxford bracketsと呼ばれる[|, |]で囲むとQuoteを生成できます。

[e| ...snip... |] :: Q Exp   -- Expression, 式のQuoteモナド生成
[d| ...snip... |] :: Q [Dec] -- Declaration, top-level宣言のQuoteモナド生成
[t| ...snip... |] :: Q Type  -- Type, 型のQuoteモナド生成
[p| ...snip... |] :: Q Pat   -- Pattern, パターンのQuoteモナド生成

これらQuoteを解釈して実行するのが所謂evalですね。
THでは$()で囲むとQuoteがevalされます。
ここでevalというのはLispの表現で、THではSplices(接合)と呼ばれているようです。構文木の中にQモナドを接合するイメージでしょうか。


以下ひたすらHello World

HaskellHello Worldです。こいつをQモナドを使って色々書いてみます。

main = putStrLn "Hello, TH?" -- #=> Hello, TH?

Quoteをすぐevalしているだけ。元と殆ど変わりません。

main = $([e| putStrLn "Hello, TH!" |]) -- #=> Hello, TH!

[e| ... |]のeは省略できます。

main = $([| putStrLn "Hello, TH!" |]) -- #=> Hello, TH!

Top-level宣言。eとは違って、d,t,pは省略不可能です。

$([d| main = putStrLn "Hello, TH!" |]) -- #=> Hello, TH!

Top-levelでの$()は省略出来ます。(これ知らないと一般のコードでTH見たとき意味分かりません...)

[d| main = putStrLn "Hello, TH!" |] -- #=> Hello, TH!

eは式を書ける場所にしか置くことが出来ません。
d, t, pについても同じで、それぞれ置ける場所が決まっています。

-- error!!!
$([e| putStrLn "Hello, TH!" |]) -- Top-levelにexpressionは書けない

putStrLnを分解。
varEとmkName。
mkNameはStringからNameを生成し、
varEはNameから式のQモナドを生成します。
Qモナドなら、$()でevalことが出来ます。eval相当の$()と、関数適用演算子($)は別物。

-- mkName :: String -> Name
-- varE :: Name -> Q Exp
main = $([| $(varE $ mkName "putStrLn") "Hello, TH!" |]) -- #=> Hello, TH!

Nameはquote(')をsymbol(?)の前につけることでも生成出来る。関数の場合。

main = $([| $(varE $ 'putStrLn) "Hello, TH!" |]) -- #=> Hello, TH!

文字列リテラル"Hello, TH!"のQモナド表現。

-- litE :: Lit -> Q Exp
-- stringL :: String -> Lit
main = $([| $(varE $ mkName "putStrLn") $(litE (stringL "Hello, TH!")) |]) -- #=> Hello, TH!

関数適用appE。

-- appE :: Q Exp -> Q Exp -> Q Exp
main = $([| $(appE (varE $ mkName "putStrLn") (litE (stringL "Hello, TH!"))) |]) -- #=> Hello, TH!

ここまでくると最外の$([| ... |])いりませんね。

main = $(appE (varE $ mkName "putStrLn") (litE (stringL "Hello, TH!"))) -- #=> Hello, TH!

なんとなく雰囲気はつかめたような。
使い方をまとめてみます。

  1. HaskellのコードをOxford brackets[|, |]で囲うと、Qモナドが生成される(Quote)
  2. Haskellコード上でQモナドを$()で囲うと、QモナドHaskellコードに変換される(Eval/Splice)
  3. 1,2は互いにネストすることが可能
  4. Top-levelの$()は省略可能

まあ処理に直結した理解ではないでしょうけど、使用時の考え方としてはこれでだいたい良いですかね。

THを使った最も単純で需要のあることといったら、Stringからその文字列相当の関数を呼び出すことでしょうか。

main = $(varE $ mkName "hello") -- 呼び出し側
hello = putStrLn "Hello, TH!"   -- 呼び出される関数

Qモナドは実行時ではなくコンパイル時にHaskellのコードに変換されてしまうので注意が必要ですが。

実用例

昨今の最も熱いHaskellのWebフレームワーク、YesodはTemplate Haskellを効果的に用いています。
YesodのHello, Worldを初めに見た時は訳の分からなさに驚いたものです。こんな難しいHello, Worldがあるのかと。
だけどTemplate Haskellがどういうものか分かっていればそういう違和感もないかもしれませんね。

以下がYesodのHello, Worldです。
本エントリでは取り上げていないQuasiQuotes(準クォート)を用いています。Oxford bracketsの中にe,d,t,p以外のモノが入っていますね。QuasiQuotesを用いると、オレオレDSLが簡単につくれるとかなんとか。

{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings #-}
import Yesod
data HelloWorld = HelloWorld
mkYesod "HelloWorld" [parseRoutes|
/ HomeR GET
|]
instance Yesod HelloWorld where
    approot _ = ""
getHomeR = defaultLayout [whamlet|Hello World!|]
main = warpDebug 3000 HelloWorld