Template Haskellでタプル版map, mapMっぽいものを作ってみた。
小ネタ。
Mainモジュールに次のようなコード
(body, width, height) <- (,,) <$> newIORef "" <*> newIORef 300 <*> newIORef 0 (tlBody, tlWidth, tlHeight) <- (,,) <$> readIORef body <*> readIORef width <*> readIORef height
があり、ふと「タプル版mapMみたいなものがあればもっとすっきり書けないか?」と思い、Template Haskellの練習がてらタプル版のmapとmapMを作ってみました。2,3コのタプルにだけ定義するのもつまらなかったので、個数は一般としました。
Template Haskellの書き方はこの記事が大変参考になりました。
できる!Template Haskell(完)- はてな使ったら負けだと思っている deriving Haskell
http://haskell.g.hatena.ne.jp/mr_konn/20111218/1324220725
まずは任意長のタプルに対するmap, mapT(uple)を。ちなみにインターフェースはmapT, mapMT共にInt(タプルの長さ) -> ExpQ(適用する関数) -> ExpQとしました。$(mapT 3 [|show|]) (1, '2', "3")みたいな使い方を想定しています。
参考記事に倣ってまずは(\(x, y, z) -> (f x, f y, f z))の構文木を調べると
LamE [TupP [VarP x_3,VarP y_4,VarP z_5]] (TupE [AppE (VarE f_1627404183) (VarE x_3),AppE (VarE f_1627404183) (VarE y_4),AppE (VarE f_1627404183) (VarE z_5)])
となっており、整理して個数を一般化すると
(f :: Exp とする)
LamE
[TupP [VarP x_0,VarP x_1, ..]]
(TupE
[AppE f (VarE x_0),AppE f (VarE x_1), ..]
)
となります。この構文木を生成するプログラムを作ればいいのでこんな感じになります。
fleshな識別子の供給は
Language.Haskell.TH.Syntax.newName :: String -> Q Name
で行い、構文木の生成には構文木の構成子の頭文字を小文字にしたものを使えばOKです。
これを外部ライブラリとして読んで、次のように使えます。
> $(mapT 3 [|show|]) (1,[2,3],'4')
("1","[2,3]","'4'")
> $(mapT 3 [|(:[])|]) (1,[2,3],'4')
([1],[[2,3]],"4")
式クオートとして渡しているので、上のように返り値の型がタプルの各要素で異なっていても使えます。
次にmapMを書きます。 \(x,y,z) -> (<*>) *1 (f y)) (f z) の構文木を調べると
LamE [TupP [VarP x_6,VarP y_7,VarP z_8]] (AppE (AppE (VarE Control.Applicative.<*>) (AppE (AppE (VarE Control.Applicative.<*>) (AppE (AppE (VarE Data.Functor.<$>) (ConE GHC.Tuple.(,,))) (AppE (VarE f_1627397566) (VarE x_6)))) (AppE (VarE f_1627397566) (VarE y_7)))) (AppE (VarE f_1627397566) (VarE z_8)))
これを整理して個数について一般化すると、
LamE
[TupP [VarP x_0,VarP x_1, ..]]
(AppE (AppE (VarE <*>)
..
(AppE (AppE (VarE <*>)
(AppE (AppE (VarE <*>)
(AppE (AppE (VarE <$>)
(ConE (,, ..)))
(AppE f (VarE x_0)))
(AppE f (VarE x_1))))
(AppE f (VarE x_2))))
..
(AppE f (VarE x_n))))
わかりづらいですが、(ConE (,, ..))を開始値として
\x y ->
(AppE (AppE (VarE <*>【または <$>】)
x)
(AppE f (VarE y)))
を演算として変数のリストを畳み込んだ形になっています。
厄介なのは最初のみ<*>ではなく<$>である場合があることですが、演算子のリスト[<$>, <*>, <*>, ..]をzipして一緒に畳み込むことで解決しました。ちょっと強引な解決法ですが……
これを用いて、最初のboilerplateはこんなふうに書けます。
(body, width, height) <- $(mapMT 3 [|newIORef|]) ("", 300, 0)
(tlBody, tlWidth, tlHeight) <- $(mapMT 3 [|readIORef|]) (body, width, height)
なかなかすっきりしました。newIORefやreadIORefが一箇所にまとまっているのがいい感じです。
というわけでTemplate Haskellを試してみたという記事でした。
今後も使えるところがあれば使っていこうと思いますので、THUtilityモジュールとして専用のモジュールにTemplate Haskellを用いたユーティリティをまとめておくことにしました。