Template Haskellでコンパイル時にFizzBuzz

Template Haskellでコンパイル時にFizzBuzz
======================================

Template Haskellとは
--------------------

Template HaskellとはHaskellの「マクロ」だ。コンパイル時に展開されるコードだ。多くの言語で「マクロ」は文字列として展開される。

[GitHub: C言語とHaskellのマクロ](https://github.com/YoshikuniJujo/test_haskell/tree/master/features/template_haskell/various_macros)

### C言語のマクロ

たとえば、C言語のマクロを見てみよう。

```c:c_macro.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;
}
```

コンパイルして実行する。

```console
% 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では「構文木」に展開される。あるいは「構文木」として書かれたマクロが挿入される。

```hs:Foo.hs
{-# 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)
```

```hs:useFoo.hs
{-# 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)
```

コンパイルして実行する。

```console
% 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のマクロ定義の部分を、みてみよう。

```hs
foo n = infixE
        (Just . litE $ integerL n)
        (varE '(+))
        (Just . litE $ integerL 3)
```

等号の右辺は構文木を組み立てている。integerL 3は数値リテラルである3を意味する。litEはリテラルを式(Exp)にする。varEは変数を式にする。infixEは演算子の適用を意味する。infixEの第1引数と第3引数がMaybe値なのは、演算子の部分適用に対応しているためだ。たとえば(+ 3)であれば、以下のように表現される。

```hs
infixE
        Nothing
        (varE '(+))
        (Just . litE $ integerL 3)
```

また、'(シングルクォート)をつけることで、その環境で定義されている変数の名前を、マクロ内で簡潔に表記することができる。

コンパイル時に評価
----------------

値を実行時ではなく、コンパイル時に評価してしまうことで、効率の最適化が可能だ。たとえば「[アッカーマン関数](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%83%E3%82%AB%E3%83%BC%E3%83%9E%E3%83%B3%E9%96%A2%E6%95%B0)の(3, 12)を表示するコマンドが作りたいなぁ」と思ったとする。

[GitHub: アッカーマン関数の例](https://github.com/YoshikuniJujo/test_haskell/tree/master/features/template_haskell/ackermann)

### モジュールAckermann

まずは、アッカーマン関数を公開するモジュールを作成しよう。

```hs:Ackermann.hs
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))
```

### マクロを使わない場合

マクロを使わないとすると、つぎのようになる。

```hs:ack.hs
import Ackermann

main :: IO ()
main = print $ ackermann 3 12
```

コンパイルして実行する。

```console
% stack ghc -- ack.hs -o ack
% time ./ack
32765
./ack 81.56s user 0.59s system 63% cpu 2:10.39 total
```

(僕の非力なマシンでは)2分以上かかった。

### マクロを使う場合

さて、この計算をコンパイル時にやってしまうことにしよう。

```hs:ackWithTh.hs
{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.TH

import Ackermann

main :: IO ()
main = print $(litE . integerL $ ackermann 3 12)
```

コンパイルする。このとき、さきにAckermann.oを削除しておく必要がある。

```console
% 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
```

実行してみよう。

```console
% time ./ackWithTh
32765
./ackWithTh 0.00s user 0.00s system 34% cpu 0.003 total
```

コンパイル時に計算ずみなので、このコマンドは単に32765を表示しているだけだ。

文字列をテキストファイルから取得
----------------------------

長めの文章をコードのなかに書きたいことがある。そんなとき、文字列をテキストファイルから取得できると便利だ。もちろん、実行時にファイルから読み込んでもいいが、その場合、(CabalとかStackとかで)ちゃんとしたインストーラを書かないと、そのファイルがちゃんと配置できない。そんなおおげさな話でない場合、実行可能ファイル内に文字列も入れておきたいと思うだろう。そんなときにも、Template Haskellは便利に使える。

[GitHub: 般若心経を表示するコマンド](https://github.com/YoshikuniJujo/test_haskell/tree/master/features/template_haskell/heart_sutra)

### 般若心経を表示するコマンド

たとえば、ぐちゃぐちゃのコードを読んでいて、処理の流れがコードの海の真ん中で行方不明になったりしたときなど、心をしずめる必要があるケースは多い。そのようなとき、般若心経を表示するコマンドがあると便利だ。

```heartSutra.hs
{-# LANGUAGE TemplateHaskell #-}

import Control.Monad.IO.Class
import Language.Haskell.TH

main :: IO ()
main = putStr $(litE . stringL =<< liftIO (readFile "heartSutra.txt"))
```

```heartSutra.txt
仏説摩訶般若波羅蜜多心経
観自在菩薩行深般若波羅蜜多時照見五蘊皆空
度一切苦厄舎利子色不異空空不異色色即是空
空即是色受想行識亦復如是舎利子是諸法空相
不生不滅不垢不浄不増不減是故空中無色無受
想行識無眼耳鼻舌身意無色声香味触法無眼界
乃至無意識界無無明亦無無明尽乃至無老死亦
無老死尽無苦集滅道無智亦無得以無所得故菩
提薩埵依般若波羅蜜多故心無罣礙無罣礙故無
有恐怖遠離一切顛倒夢想究竟涅槃三世諸仏依
般若波羅蜜多故得阿耨多羅三藐三菩提故知般
若波羅蜜多是大神呪是大明呪是無上呪是無等
等呪能除一切苦真実不虚故説般若波羅蜜多呪
即説呪曰
羯諦羯諦波羅羯諦波羅僧羯諦菩提薩婆訶
般若心経
```

コンパイルして実行してみよう。

```console
% stack ghc -- heartSutra.hs -o heartSutra
% ./heartSutra
仏説摩訶般若波羅蜜多心経
観自在菩薩行深般若波羅蜜多時照見五蘊皆空
...
```

### コードの解説

値mainの定義の右辺は、つぎのようになっている。

```hs
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をすることができる。

[GitHub: コンパイル時のFizz Buzz](https://github.com/YoshikuniJujo/test_haskell/tree/master/features/template_haskell/fizzbuzz)

```fizzbuzz.hs
{-# 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 ()
```

コンパイルしてみる。

```console
% 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が必要になる。

```console
% 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のバージョンの変化についていくのに、あるていどの、労力が必要になるかもしれない。