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記法は内部的にはほとんど同じなんだとか、いろいろ面白い発見があると思います。
個人的にポイントだと思うところは
>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でもいいのですが、あえてエラーにしました