Template Haskellでコンパイル時にFizzBuzz
Template Haskellとは
Template HaskellとはHaskellの「マクロ」だ。コンパイル時に展開されるコードだ。多くの言語で「マクロ」は文字列として展開される。
C言語のマクロ
たとえば、C言語のマクロを見てみよう。
#include <stdio.h>
#define foo(x) x + 3
int
main(int argc, char *argv[])
{
printf("foo(5) - 2 = %d\n", foo(5) - 2);
printf("foo(5) * 2 = %d\n", foo(5) * 2);
return 0;
}
コンパイルして実行する。
% gcc c_macro.c -o c_macro
% ./c_macro
foo(5) - 2 = 6
foo(5) * 2 = 11
foo(5)は"5 + 3"に展開されるので、foo(5) - 2は5 + 3 - 2で6になる。またfoo(5) * 2は5 + 3 * 2で11になる。C言語のマクロは文字列として展開されるので。
このように「コンパイル前」にコードを生成することができる。
Template Haskellでは「構文木」に展開される
C言語のマクロでは「文字列」として展開されるが、Template Haskellでは「構文木」に展開される。あるいは「構文木」として書かれたマクロが挿入される。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
{-# LANGUAGE TemplateHaskell #-}
module Foo (foo) where
import Language.Haskell.TH
foo :: Integer -> ExpQ
foo n = infixE
(Just . litE $ integerL n)
(varE '(+))
(Just . litE $ integerL 3)
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
{-# LANGUAGE TemplateHaskell #-}
import Foo
main :: IO ()
main = do
putStrLn $ "foo(5) - 2 = " ++ show ($(foo(5)) - 2 :: Integer)
putStrLn $ "foo(5) * 2 = " ++ show ($(foo(5)) * 2 :: Integer)
コンパイルして実行する。
% stack ghc -- useFoo.hs -o useFoo
% ./useFoo
foo(5) - 2 = 6
foo(5) * 2 = 16
マクロ展開する部分は$(...)のように先頭に$をつける。Template Haskellでは構文木に展開されるので、foo(5) * 2は(5 + 3)を表す構文木に展開されて、それに* 2がつくので、結果は16になる。
Haskellのマクロの解説
Haskellのマクロ定義の部分を、みてみよう。
foo n = infixE
(Just . litE $ integerL n)
(varE '(+))
(Just . litE $ integerL 3)
等号の右辺は構文木を組み立てている。integerL 3は数値リテラルである3を意味する。litEはリテラルを式(Exp)にする。varEは変数を式にする。infixEは演算子の適用を意味する。infixEの第1引数と第3引数がMaybe値なのは、演算子の部分適用に対応しているためだ。たとえば(+ 3)であれば、以下のように表現される。
infixE
Nothing
(varE '(+))
(Just . litE $ integerL 3)
また、'(シングルクォート)をつけることで、その環境で定義されている変数の名前を、マクロ内で簡潔に表記することができる。
コンパイル時に評価
値を実行時ではなく、コンパイル時に評価してしまうことで、効率の最適化が可能だ。たとえば「アッカーマン関数の(3, 12)を表示するコマンドが作りたいなぁ」と思ったとする。
モジュールAckermann
まずは、アッカーマン関数を公開するモジュールを作成しよう。
module Ackermann (ackermann) where
ackermann :: Integer -> Integer -> Integer
ackermann 0 n = n + 1
ackermann m 0 = ackermann (m - 1) 1
ackermann m n = ackermann (m - 1) (ackermann m (n - 1))
マクロを使わない場合
マクロを使わないとすると、つぎのようになる。
import Ackermann
main :: IO ()
main = print $ ackermann 3 12
コンパイルして実行する。
% stack ghc -- ack.hs -o ack
% time ./ack
32765
./ack 81.56s user 0.59s system 63% cpu 2:10.39 total
(僕の非力なマシンでは)2分以上かかった。
マクロを使う場合
さて、この計算をコンパイル時にやってしまうことにしよう。
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Ackermann
main :: IO ()
main = print $(litE . integerL $ ackermann 3 12)
コンパイルする。このとき、さきにAckermann.oを削除しておく必要がある。
% rm Ackermann.o
% time stack ghc -- ackWithTh.hs -o ackWithTh
[1 of 2] Compiling Ackermann ( Ackermann.hs, Ackermann.o )
[2 of 2] Compiling Main (ackWithTh.hs, ackWithTh.o )
(ここで、2分くらい?待つ)
Linking ackWithTh ...
stack ghc -- ackWithTh.hs -o ackWithTh 58.19s user 0.54s system 75% cpu 1:17.39 total
実行してみよう。
% time ./ackWithTh
32765
./ackWithTh 0.00s user 0.00s system 34% cpu 0.003 total
コンパイル時に計算ずみなので、このコマンドは単に32765を表示しているだけだ。
文字列をテキストファイルから取得
長めの文章をコードのなかに書きたいことがある。そんなとき、文字列をテキストファイルから取得できると便利だ。もちろん、実行時にファイルから読み込んでもいいが、その場合、(CabalとかStackとかで)ちゃんとしたインストーラを書かないと、そのファイルがちゃんと配置できない。そんなおおげさな話でない場合、実行可能ファイル内に文字列も入れておきたいと思うだろう。そんなときにも、Template Haskellは便利に使える。
般若心経を表示するコマンド
たとえば、ぐちゃぐちゃのコードを読んでいて、処理の流れがコードの海の真ん中で行方不明になったりしたときなど、心をしずめる必要があるケースは多い。そのようなとき、般若心経を表示するコマンドがあると便利だ。
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad.IO.Class
import Language.Haskell.TH
main :: IO ()
main = putStr $(litE . stringL =<< liftIO (readFile "heartSutra.txt"))
仏説摩訶般若波羅蜜多心経
観自在菩薩行深般若波羅蜜多時照見五蘊皆空
度一切苦厄舎利子色不異空空不異色色即是空
空即是色受想行識亦復如是舎利子是諸法空相
不生不滅不垢不浄不増不減是故空中無色無受
想行識無眼耳鼻舌身意無色声香味触法無眼界
乃至無意識界無無明亦無無明尽乃至無老死亦
無老死尽無苦集滅道無智亦無得以無所得故菩
提薩埵依般若波羅蜜多故心無罣礙無罣礙故無
有恐怖遠離一切顛倒夢想究竟涅槃三世諸仏依
般若波羅蜜多故得阿耨多羅三藐三菩提故知般
若波羅蜜多是大神呪是大明呪是無上呪是無等
等呪能除一切苦真実不虚故説般若波羅蜜多呪
即説呪曰
羯諦羯諦波羅羯諦波羅僧羯諦菩提薩婆訶
般若心経
コンパイルして実行してみよう。
% stack ghc -- heartSutra.hs -o heartSutra
% ./heartSutra
仏説摩訶般若波羅蜜多心経
観自在菩薩行深般若波羅蜜多時照見五蘊皆空
...
コードの解説
値mainの定義の右辺は、つぎのようになっている。
putStr $(litE . stringL =<< liftIO (readFile "heartSutra.txt"))
実はTemplate Haskellで指定する構文木はQというモナドにくるまれている。このQモナドはMonadIOのインスタンスだ。つまり、IOモナドをliftIOでQモナドにもちあげることができる。readFileをQモナドにもちあげたうえで、stringLとlitEによってExpQ(Q Exp)に変換している。
コンパイル時のFizzBuzz
さて、うえの例で構文木の生成中にIOが使えることがわかった。IOが使えるということは、何であれ表示できるということだ。つまり、コンパイル時にFizz Buzzをすることができる。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
{-# LANGUAGE TemplateHaskell #-}
import Control.Monad
import Control.Monad.IO.Class
do liftIO $ forM_ [1 :: Int .. 100] $ \n -> do
putStrLn $ case (n `mod` 3, n `mod` 5) of
(0, 0) -> "FizzBuzz"
(0, _) -> "Fizz"
(_, 0) -> "Buzz"
_ -> show n
return []
main :: IO ()
main = return ()
コンパイルしてみる。
% stack ghc -- fizzbuzz.hs -o fizzbuzz
[1 of 1] Compiling Main
1
2
Fizz
4
Buzz
Fizz
7
8
.
.
.
Fizz
97
98
Fizz
Buzz
Linking fizzbuzz ...
コンパイル時にFizz Buzzができた。ちなみに、2回目以降は、つぎのように-fforce-recompが必要になる。
% stack ghc -- -fforce-recomp fizzbuzz.hs -o fizzbuzz
コードの説明
コードを再掲する。
do liftIO $ forM_ [1 :: Int .. 100] $ \n -> do
putStrLn $ case (n `mod` 3, n `mod` 5) of
(0, 0) -> "FizzBuzz"
(0, _) -> "Fizz"
(_, 0) -> "Buzz"
_ -> show n
return []
まず、トップレベルにいきなりdoが出てきて面食らったかもしれない。Template Haskellではトップレベルの$(...)は省略可能だ。ていねいに書くならdoからreturn[]までを$(...)でかこむことになるだろう。トップレベルのマクロでは関数定義などを置くことができる。ここではreturn []として空リストをかえしているが、ここに関数定義等を表す構文木を書いておけば、その定義が挿入される。
そのほかについては、ごく普通のFizz Buzzだ。
まとめ
「コンパイル時のFizz Buzz」というキャッチーなタイトルで、Template Haskellを紹介してみた。ざっくりとした紹介なので、この記事だけではTemplate Haskellが書けるようにはならないが、「おもしろそうな機能だな」と思ってもらえれば幸いだ。
ただし、実際のプロジェクトで使うには「強力な機能」であるぶん、コードの可読性や保守性の低下などの副作用があるように思う。慎重に使う必要がある。また、構文木はGHCのバージョンによって、変化していくため、Template Haskellを利用したコードではGHCのバージョンの変化についていくのに、あるていどの、労力が必要になるかもしれない。