28
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Template Haskellでコンパイル時にFizzBuzz

Last updated at Posted at 2018-08-28

Template Haskellでコンパイル時にFizzBuzz

Template Haskellとは

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

GitHub: C言語とHaskellのマクロ

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;
}

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

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

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)
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)

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

% 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)を表示するコマンドが作りたいなぁ」と思ったとする。

GitHub: アッカーマン関数の例

モジュールAckermann

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

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))

マクロを使わない場合

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

ack.hs
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分以上かかった。

マクロを使う場合

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

ackWithTh.hs
{-# 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は便利に使える。

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

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

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

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

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

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

GitHub: コンパイル時のFizz Buzz

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 ()

コンパイルしてみる。

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

28
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?