Template Haskellのサンプル

どう書くorgに良い題材があったので、それを使ってTemplate Haskellのコードを書いてみました。
問題は全ての組み合わせです。

Haskellではリスト内の要素の型は固定なので、組み合わせはタプルで表すことにします。
また、可変長引数も無いのをどうにかします。

Applicativeでの解法

自分が思う最も良い解法はApplicativeを使うものです。

>(,) <$> [1, 2] <*> ["foo", "bar", "baz"]
[(1,"foo"),(1,"bar"),(1,"baz"),(2,"foo"),(2,"bar"),(2,"baz")]
>(,,) <$> [1, 2] <*> ["foo", "bar", "baz"] <*> [True, False]
[(1,"foo",True),(1,"foo",False),(1,"bar",True),(1,"bar",False),
(1,"baz",True),(1,"baz",False),(2,"foo",True),(2,"foo",False),
(2,"bar",True),(2,"bar",False),(2,"baz",True),(2,"baz",False)]

Template Haskellでの解法

コンパイル時に引数の数に従って、目的のコードを生成できるようにしてみます。

{-# OPTIONS_GHC -fth #-}
import Language.Haskell.TH
import Control.Applicative
import Control.Monad

varList :: Int -> String -> [Name]
varList n s = [mkName (s ++ show i) | i <- [1..n]]

crossProduct :: Int -> Q Exp
crossProduct n = do
    when (n < 2) (report True "argument of crossProduct must be greater than 1")
        -- 一時変数を2組使用
    let xs = varList n "x"
        ps = varList n "p"
        -- \p1 p2 ... -> [(x1, x2, ..) | x1 <- p1, x2 <- p2, ...] というコードを生成
     in return $ LamE (VarP <$> ps) 
                      (CompE $ (zipWith BindS (VarP <$> xs) (VarE <$> ps)) 
                  ++ [NoBindS $ TupE (VarE <$> xs)])

実行例

>$(crossProduct 2) [1, 2] ["foo", "bar", "baz"]
[(1,"foo"),(1,"bar"),(1,"baz"),(2,"foo"),(2,"bar"),(2,"baz")]
>$(crossProduct 3) [1, 2] ["foo", "bar", "baz"] [True, False]
[(1,"foo",True),(1,"foo",False),(1,"bar",True),(1,"bar",False)
,(1,"baz",True),(1,"baz",False),(2,"foo",True),(2,"foo",False)
,(2,"bar",True),(2,"bar",False),(2,"baz",True),(2,"baz",False)]

最終的に生成するコードは要素数がnの場合

\p1 p2 ... pn -> [(x1, x2, ..., xn) | x1 <- p1, x2 <- p2, ..., xn <- pn]

という式です。p1 .. p2にリストを渡すと、それらから一つずつ取り出したものをタプルにして集めます。
上の関数の構文木はnが2の場合

>runQ [| \p1 p2 -> [(x1, x2) | x1 <- p1, x2 <- p2] |]
LamE [VarP p1_0,VarP p2_1] (CompE [BindS (VarP x1_2) (VarE p1_0)
,BindS (VarP x2_3) (VarE p2_1),NoBindS (TupE [VarE x1_2,VarE x2_3])])

となります。

このrunQで生成したものを見ながら組み立てていけば良いですが、少しExp型について説明します。

Exp型

まず、[| |]内の全体構造は

LamE (引数のリスト) (関数本体)

の形です。

..Eとなっているものは式(Exp型)で、..Pとなっているものはパターン(Pat型)です。
p1_0, p2_1などと数字がついているのは同じ名前がコード全体で重複して出現しないようにQuotation Monadというものが自動的に割り振っている数字です。

(パターン) <- (式)というコードは内部表現では

BindS (パターン) (式)

となり、(式)の部分しかなければ

NoBindS (式)

となります。この..Sという形はステートメント(Stmt型)で、以下の4つがあります。do記法やリスト内包表記は内部的に..Sというデータのリストとして表されます。

let x = .., y = ...  => LetS [Dec] 
(パターン) <- (式)      => BindS Pat Exp 
(式)                   => NoBindS Exp
par (ステートメント) (ステートメント) ..  => ParS [[Stmt]]

最後のparは並列処理を記述する為のものだと思います(Parallel Haskellのpar関数(?))が、調べていません。また、Template Haskellではまだ未実装の構文です。

Decは関数定義やデータ定義やクラス定義やinstance定義などです。これらを使うともっと面白い事ができますが、とりあえず今日はやりません。

mkNameとnewName

変数名に直接Stringを使うことはできないので、Name型への変換が必要です。
まずnewNameという関数がありますが、これは上で説明したように新しい番号を割り振ったものを返します。内部でカウンタをインクリメントするという副作用を起こしていますので、Qモナドを返します。

>runQ $ newName "x"
x_8

これは、新しい変数や関数を定義する場合に使用する事が多いです。通常は

do var <- newName "x"
   ...
   VarP var

みたいに、保存して使います。

次にmkNameという関数ですが、これはすでにある変数を参照する場合などに使います。newNameと違い副作用はないので、QモナドではなくNameを直接返します。もう一つの違いは'.'が使えます。

>mkName "x"
x
>mkName "Prelude.id"
Prelude.id

mkNameの生成する変数が指す実体は文脈により異なります。例えば同じmap関数でもPrelude.mapかMap.mapかなどはその時importしているモジュールにより決まるわけです。

先ほどrunQして得た構文木ではすべてnewNameが使われていますが、Haskellでは内側のスコープが優先されるのでmkNameでも特に問題ありません。mkNameの方が使いやすい気がします。

report

reportを使用すると、コンパイル時にエラーを検出しそれを報告することができます。

report True "..."

とした場合はエラーを意味します、コンパイル時にreportが実行されると、エラーを表示しそこでコンパイルが終了します。
コンパイルを終了せずに復帰して、できるだけ多くのエラーを見つけたいとか思う場合はrecoverという関数が使えます。

report False "..."

とした場合は警告(や単なるレポート等)を意味します、コンパイル時にメッセージが表示されますが、最後までコンパイルは進みます。*1

これとrecoverの違いはエラーが発生した場合に、復帰するための処理をrecoverは行えるのに大して、単にreport Falseにした場合は素通りするだけです。

なんというか、テンプレートメタプログラミングというよりコンパイラの内部を直接触っているような感じがします。面白いです。

ポイント

Template Haskell構文木は結構複雑なので、runQでいろいろ見てみて、単純なものを探すようにするといいです。
リスト内包表記を使用しているのもこれが一番構文木が簡単だからです。

できればLanguage/Haskell/TH/Syntax.hsのコードを直接読んでみることをおすすめします。実はリスト内包表記とdo記法は内部的にはほとんど同じなんだとか、いろいろ面白い発見があると思います。

個人的にポイントだと思うところは

  • 演算子演算子の部分適用は不必要に使用しない方がいい。
    • Maybeの処理が面倒。
    • 演算子を関数化して使用するとAppE (AppE (AppE ...) ...)...)という形の構文木になって、再帰処理に適した形になる。
>runQ [| 1 + 2 |]
InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2)))
>runQ [| (1 +) 2 |]
AppE (InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) Nothing) (LitE (IntegerL 2))
>runQ [| (+) 1 2 |]
AppE (AppE (VarE GHC.Num.+) (LitE (IntegerL 1))) (LitE (IntegerL 2))
  • 構文木そのものがリストで表されるものを利用した方が楽。
    • 素数が変わっても、リストならば簡単に処理できる。
    • タプルも内部的にはリスト

リテラルのリストや変数のリストなどを作りたかったらApplicativeが使いやすいかもしれないです。

>map LitE $ map IntegerL $ [1, 2, 3, 4]
[LitE (IntegerL 1),LitE (IntegerL 2),LitE (IntegerL 3),LitE (IntegerL 4)]
より
>LitE <$> IntegerL <$> [1, 2, 3, 4]
[LitE (IntegerL 1),LitE (IntegerL 2),LitE (IntegerL 3),LitE (IntegerL 4)]

[追記]

mkNameの方が使いやすいと書きましたが、newNameを使えば自動的に連番を振ってくれるので、わざわざvarList使わなくていいですね。こっちのほうが楽でした。

あと、LamE ...の部分はもっと簡単に書く方法がありました。もうちょっと調べて明日書きます。

*1:実は1要素のタプルというものが存在するのでn == 1でもいいのですが、あえてエラーにしました