Template Haskell の例
追記: さらに更新(6/23)。
追記: ちょっと更新。
Template Haskell を以下のように進めている。
- 生成したいボイラープレートの一部を書き下し、コンパイルが通ることを確認する。
- インデントによるレイアウト規則を使っている場合は、次に行う構文木の調査のために、波括弧とセミコロンで書き直しておく。
- 生成したいコードを runQ に渡し、構文木を調べる。
- 出力された構文木の構造を調べる。
- 出力された構文木をそのまま返す関数を、サンプルとして書く。
- コンパイルが通ることを確認したら、生成したいボイラープレートを意識しつつ、リファクタリングする。
- ボイラープレートを無くす。
以下は実例。
- 生成したいボイラープレートの一部を書き下し、コンパイルが通ることを確認する。
-- 例。この T1 以外に引数が 2つの T2 a b,3つの T3 a b cと多数の類似のコードがあり、TH で生成したい。 instance (Typeable a, Show a) => Tuple (T1 a) where heading (T1 (A a _)) = [a] amap f (T1 (A a x)) = T1 (A (f a) x) tvalue (T1 (A _ x)) = [show x] degree _ = 1 typeNames (T1 (A _ x)) = [typeOf x]
instance (Typeable a, Show a) => Tuple (T1 a) where { heading (T1 (A a _)) = [a] ; amap f (T1 (A a x)) = T1 (A (f a) x); tvalue (T1 (A _ x)) = [show x]; degree _ = 1; typeNames (T1 (A _ x)) = [typeOf x];}
- 生成したいコードを runQ に渡し、構文木を調べる。この際、コードはオックスフォード角括弧と呼ばれるカッコで囲む。必要なコードの種別に応じたカッコを用いる必要があり、今回の場合はトップレベルの宣言なので d を使う。構文木は Q [Dec] 型となる。
>>> :t [d| instance (Typeable a, Show a) => Tuple (T1 a) where { heading (T1 (A a _)) = [a] ; amap f (T1 (A a x)) = T1 (A (f a) x); tvalue (T1 (A _ x)) = [show x]; degree _ = 1; typeNames (T1 (A _ x)) = [typeOf x];} |] [d| instance (Typeable a, Show a) => Tuple (T1 a) where { heading (T1 (A a _)) = [a] ; amap f (T1 (A a x)) = T1 (A (f a) x); tvalue (T1 (A _ x)) = [show x]; degree _ = 1; typeNames (T1 (A _ x)) = [typeOf x];} |] :: Q [Dec] >>> runQ [d| instance (Typeable a, Show a) => Tuple (T1 a) where { heading (T1 (A a _)) = [a] ; amap f (T1 (A a x)) = T1 (A (f a) x); tvalue (T1 (A _ x)) = [show x]; degree _ = 1; typeNames (T1 (A _ x)) = [typeOf x];} |] [InstanceD [ClassP Data.Typeable.Typeable [VarT a_0],ClassP GHC.Show.Show [VarT a_0]] (AppT (ConT RT.Tuple) (AppT (ConT RT.T1) (VarT a_0))) [FunD heading [Clause [ConP RT.T1 [ConP TH.A [VarP a_1,WildP]]] (NormalB (ListE [VarE a_1])) []],FunD amap [Clause [VarP f_2,ConP RT.T1 [ConP TH.A [VarP a_3,VarP x_4]]] (NormalB (AppE (ConE RT.T1) (AppE (AppE (ConE TH.A) (AppE (VarE f_2) (VarE a_3))) (VarE x_4)))) []],FunD tvalue [Clause [ConP RT.T1 [ConP TH.A [WildP,VarP x_5]]] (NormalB (ListE [AppE (VarE GHC.Show.show) (VarE x_5)])) []],FunD degree [Clause [WildP] (NormalB (LitE (IntegerL 1))) []],FunD typeNames [Clause [ConP RT.T1 [ConP TH.A [WildP,VarP x_6]]] (NormalB (ListE [AppE (VarE Data.Typeable.typeOf) (VarE x_6)])) []]]]
- 出力された構文木をそのまま返す関数(ここでは仮に defT1Ex :: Q [Dec]とする)を、サンプルとして書く。GHC の制限で、トップレベルの宣言は import されなければならないので、構文木を返す関数は、利用するファイルとは別のファイルに書いておく。また、構文木に以下の修正を行なう。
- a_0 や a_1, f_1 などの名前は mkName で作った Name データにおきかえる。※変数名のキャプチャに注意。newName を使う方法がより正しい。
- ライブラリ関数や型名も mkName で作った Name データにおきかえる。ただし、mkName "GHC.Show.Show" ではうまくいかない。 mkName "Show" とする。
-- コード生成を定義しているファイル x.hs defT1Ex :: Q [Dec] defT1Ex = return defT1Ex' defT1Ex' = [InstanceD [ClassP typeable [VarT a_0], ClassP showClass [VarT a_0]] -- Cxt (AppT (ConT tuple) (AppT (ConT t1) (VarT a_0))) -- Type [ FunD (mkName "heading") [Clause [ConP t1 [ConP conA [VarP a_1, WildP]]] (NormalB (ListE [VarE a_1])) []], FunD (mkName "amap") [Clause [VarP f_2,ConP t1 [ConP conA [VarP a_3,VarP x_4]]] (NormalB (AppE (ConE t1) (AppE (AppE (ConE conA) (AppE (VarE f_2) (VarE a_3))) (VarE x_4)))) []], FunD (mkName "tvalue") [Clause [ConP t1 [ConP conA [WildP,VarP x_5]]] (NormalB (ListE [AppE (VarE showFunc) (VarE x_5)])) []], FunD (mkName "degree") [Clause [WildP] (NormalB (LitE (IntegerL 1))) []], FunD (mkName "typeNames") [Clause [ConP t1 [ConP conA [WildP,VarP x_6]]] (NormalB (ListE [AppE (VarE typeof) (VarE x_6)])) []]] ] where conA = mkName "A" showClass = mkName "Show" showFunc = mkName "show" typeable = mkName "Data.Typeable.Typeable" typeof = mkName "Data.Typeable.typeOf" tuple = mkName "Tuple" t1 = mkName "T1" a_0 = mkName "a_0" a_1 = mkName "a_1" a_2 = mkName "a_2" a_3 = mkName "a_3" f_1 = mkName "f_1" f_2 = mkName "f_2" x_4 = mkName "x_4" x_5 = mkName "x_5" x_6 = mkName "x_6" -- コード生成を利用するファイル y.hs ... $(defT1Ex) -- トップレベル宣言は $() は省略可能で、 defT1Ex と書いてもよい。 ...
-
- ppr 関数を使うと、展開形を出力できるので適宜用いて確認する。
>>> ppr defT1Ex' instance (Data.Typeable.Typeable a_0, Show a_0) => Tuple (T1 a_0) where heading (T1 (A a_1 _)) = [a_1] amap f_2 (T1 (A a_3 x_4)) = T1 (A (f_2 a_3) x_4) tvalue (T1 (A _ x_5)) = [show x_5] degree _ = 1 typeNames (T1 (A _ x_6)) = [Data.Typeable.typeOf x_6]
-
- ppr 関数は Q モナドに対してはそのままは使えない。runQ で実行したのち pprint する関数 expand を作っておくと簡単に展開形を確認できる。
expand x = runQ x >>= putStrLn . pprint
- コンパイルが通ることを確認したら、生成したいボイラープレートを意識しつつ、リファクタリングする。どう生成したらいいか不明な場合は、サンプルを増やして確認する。複雑な構文木を作る場合は、単純化するために別の関数として切り出すと間違えにくい。
-
- リファクタリング時には等価な、より生成しやすい単純な式に変形することを考える。
- 引数が一つより多い場合の関数適用、たとえば f x0 x1 x2 は (((f x0) x1) x2) である。foldl を使うと簡単に作成できる。
- リファクタリング時には等価な、より生成しやすい単純な式に変形することを考える。
>>> runQ [d| f x0 x1 x2 = (((f x0) x1) x2) |] [FunD f [Clause [VarP x0_9,VarP x1_10,VarP x2_11] (NormalB (AppE (AppE (AppE (VarE f) (VarE x0_9)) (VarE x1_10)) (VarE x2_11))) []]] >>> FunD (mkName "f") [Clause (map (\x -> VarP (mkName ("a" ++ show x))) [1..3]) (NormalB (foldl AppE (VarE (mkName "f")) (map (\x -> VarE (mkName ("a" ++ show x))) [1..3]))) []] FunD f [Clause [VarP a1,VarP a2,VarP a3] (NormalB (AppE (AppE (AppE (VarE f) (VarE a1)) (VarE a2)) (VarE a3))) []] >>> ppr it f a1 a2 a3 = f a1 a2 a3
-
- where 句を使うことで単純にできる場合がある。
- ghci にオプション -ddump-splices を付けると、splice 時に展開結果を出力してくれる。
- ボイラープレートを無くす。
ボイラープレートを生成する関数としてまとめ、不要となったボイラープレートを削除する。
参考資料
- http://haskell.g.hatena.ne.jp/mr_konn/20111218/1324220725 できる!Template Haskell (完) とても参考になる記事。
- http://www.kotha.net/ghcguide_ja/7.6.2/template-haskell.html Template Haskell