時代遅れひとりFizzBuzz祭り make編(gmakeでもpmakeでも書いてみせらあ。でもnmakeだけは勘弁な!)

時代遅れひとりFizzBuzz祭り、今回はまさかのmake。Makefileで粛々とFizzBuzzする日がくるだなんて2〜3日前には想像もしていなかった。

というかmakeはプログラミング言語なのだろうか? OMakeぐらいのレベルならともかく、gmake(GNU Make)やpmake(NetBSD make)で記述したMakefileは設定ファイルっぽさが濃い目な訳で、どう頑張ってもDSLが関の山だ。

……今書いたようにmakeといっても何種類かあって、伝統的なmakeらしいmakeに限ってもgmakeとpmakeがある。Schily makeなんていうものもあるらしいけど、遭遇したことはない。

多分、比較的最近makeを使い始めた人の多くはgmakeを使っていると思う。Linuxディストリビューションにデフォルトで入っていることが多いし*1Windows上で動作するものも簡単に見つかる。ネット上で見つかるmakeのチュートリアルはgmakeでもpmakeでも使える基本的なものが多いが、少し凝ったことを解説しているものはgmakeをターゲットとしたものばかりだ。

さすがGNU。前回のObjective-Cも現行の処理系はGCCベース*2だし、Apple以外のフレームワークとなるとGNUStepぐらいしか見つからない。という訳でObjective-Cとは「実はメジャーな処理系がGNU」繋がりだと強く主張したい。

もっともFreeBSDとかの世界ではpmakeが主流なようで*3BSD畑の人がバリバリMakefileを書いているプロジェクトでは、ついうっかりgmakeでmakeしようとすると正しく動いてくれないことがある。

伝統的な機能ならともかく、便利な機能を使おうとするとgmakeもpmakeも互換性が無いところが泣けてくる。

WindowsではMicrosoft謹製のnmakeがあるが、これはこれでgmakeともpmakeとも違うようだ。もっともnmake用のMakefileは見たことはあるけど書いたことはない。何しろVisual Studioを使うならMakefileを書く必要は無いし、コンパイラとしてMinGWを使うならMSYSのgmakeを使う方が楽だ。

Windows用のmakeは他にもBorland C++ CompilerやDigital Mars C/C++ Compilerに付属のものが思い当たるが、使ったことはない。

今回FizzBuzzするにあたり、使用経験のあるgmakeとpmakeで書くことにした。OMakeは使用経験はないけど簡単にFizzBuzzできそうなので止めた。nmakeは逆にFizzBuzzできるか怪しかったので止めた。

まずはgmakeから。Ubuntu 8.04.4 LTSのGNU Make 3.81とMSYSのGNU Make 3.81(Windowsで動くもの)の双方で動作した。

# fizzbuzz1.mak
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

MAXVAL  ?= 100

FIZ     := $(shell seq 3 3 $(MAXVAL))
BUZ     := $(shell seq 5 5 $(MAXVAL))
FIZBUZ  := $(shell seq 15 15 $(MAXVAL))

TARGET  := $(shell seq 1 1 $(MAXVAL))

all:    $(TARGET)

$(FIZ):
	@echo 'Fizz'

$(BUZ):
	@echo 'Buzz'

$(FIZBUZ):
	@echo 'FizzBuzz'

.DEFAULT:
	@echo $@

ものすごく卑怯な気もするけど、gmakeの拡張機能を使ってseqを呼び出し、数列を自動生成している。

変換ルールに関しては「あるターゲットに対するルールが複数ヶ所に存在する場合、最後に発見したルールが適用される」という暗黙の仮定に基づいて記述している。例えばターゲットが `15' の場合、適用可能なルールは$(FIZ):、$(BUZ):、$(FIZBUZ):の3ヶ所に存在することになる。gmakeはどうもファイル先頭からルールを推測していき、後に発見したものを使用するようだ。

この挙動の影響で、このMakefileを普通に実行すると警告メッセージが表示される。なので以下のように実行して標準エラー出力を読み捨てることを推奨する。

# シェル上で実行する場合
make -f fizzbuzz1.mak 2>/dev/null
:: コマンドプロンプトで実行する場合
make -f fizzbuzz1.mak 2>nul

マクロを減らしてこんな風に記述しても問題ないようだ。

# fizzbuzz2.mak
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

MAXVAL  ?= 100

all:    $(shell seq 1 1 $(MAXVAL))

$(shell seq 3 3 $(MAXVAL)):
	@echo 'Fizz'

$(shell seq 5 5 $(MAXVAL)):
	@echo 'Buzz'

$(shell seq 15 15 $(MAXVAL)):
	@echo 'FizzBuzz'

.DEFAULT:
	@echo $@

先ほどのバージョンと比べて実行速度に違いがあるのだろうか? 試しに10000回FizzBuzzして計測してみた。

eel@federico:~$ uname -a
Linux federico 2.6.24-26-server #1 SMP Tue Dec 1 19:19:20 UTC 2009 i686 GNU/Linux
eel@federico:~$ time make -f fizzbuzz1.mak MAXVAL=10000 >/dev/null 2>&1

real    0m25.752s
user    0m9.380s
sys     0m16.370s
eel@federico:~$ time make -f fizzbuzz2.mak MAXVAL=10000 >/dev/null 2>&1

real    0m25.731s
user    0m9.810s
sys     0m15.920s
eel@federico:~$ _

トータルの実行速度はどちらも似たようなものだ。10000回でこの程度の差異なので、100回なら差は無いも同然だろう。

今度はpmakeで書いてみた。Ubuntu 8.04.4 LTSのpmake 1.111で動作した。

# fizzbuzz3.mak
# Ubuntu 8.04.4 LTS (server) NetBSD make (pmake) 1.111-1build1

MAXVAL  ?= 100

FIZ     := ${:!seq 3 3 $(MAXVAL)!}
BUZ     := ${:!seq 5 5 $(MAXVAL)!}
FIZBUZ  := ${:!seq 15 15 $(MAXVAL)!}

TARGET  := ${:!seq 1 1 $(MAXVAL)!}

all:    $(TARGET)

$(FIZBUZ):
	@echo 'FizzBuzz'

$(BUZ):
	@echo 'Buzz'

$(FIZ):
	@echo 'Fizz'

.DEFAULT:
	@echo $@

やっていることはgmake版と同じだ。但しseqを実行する為の書き方が異なる。こういった違いがMakefileを書くときに煩わしく感じられる所だ。

Fizz、Buzz、FizzBuzzの各ルールの記述順がgmake版と逆になっている。pmakeではあるターゲットに対するルールが複数ヶ所に存在する場合、最初に発見したルールを適用するようだ(後で発見したルールを無視する)。この挙動はgmakeとは正反対だ。

ただgmake版と同様に警告メッセージが表示されるので、標準エラー出力を読み捨てた方が見やすいという所は変わらない。

pmake版もマクロを減らしてこんな風に記述できるようだ。

# fizzbuzz4.mak
# Ubuntu 8.04.4 LTS (server) NetBSD make (pmake) 1.111-1build1

MAXVAL  ?= 100

all:    ${:!seq 1 1 $(MAXVAL)!}

${:!seq 15 15 $(MAXVAL)!}:
	@echo 'FizzBuzz'

${:!seq 5 5 $(MAXVAL)!}:
	@echo 'Buzz'

${:!seq 3 3 $(MAXVAL)!}:
	@echo 'Fizz'

.DEFAULT:
	@echo $@

実行速度はどうだろうか? gmake版と同様に10000回FizzBuzzして計測してみた。

eel@federico:~$ time pmake -f fizzbuzz3.mak MAXVAL=10000 >/dev/null 2>&1

real    0m21.120s
user    0m5.500s
sys     0m15.620s
eel@federico:~$ time pmake -f fizzbuzz4.mak MAXVAL=10000 >/dev/null 2>&1

real    0m21.191s
user    0m5.090s
sys     0m16.100s
eel@federico:~$ _

pmake版の実行速度は2つともほぼ同じだ。ただgmake版より4.5秒ほど早く終了した。まあ通常の100回FizzBuzzするケースでは気にならない程度の差だろう。

それにしてもmakeは仕事で度々使うのだけど、まさかFizzBuzzできるとは思ってもみなかった。夜中に軽い思いつきで書いてみて、結果にビックリした。

しかしFizzBuzzは書けたけど、相変わらすMakefileを書くときはリフェレンスやら何やら片手に大事業となってしまう。いつの日かすらすらと流れるようにMakefileを書けるようになれるのだろうか?*4

2012/07/13追記

やはり警告メッセージが出るのは精神的に良くない。それに何というか負けた気分になる。なので警告メッセージが出ないように改造してみた。

gmake版もpmake版も警告メッセージが出る理由は「あるターゲットに対するルールが複数存在するから」だ。具体的にはFizzないしBuzzと表示するターゲット群の中にFizzBuzzと表示するターゲットが紛れ込んでいることが原因なので、紛れ込まないようにすればよい。

例えばFizzと表示するターゲットの場合、3の倍数の集合から5の倍数の集合を取り除く。Buzzの場合はその逆になる。

gmake版の修正は比較的簡単だ。

# fizzbuzz5.mak
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1
MAXVAL  ?= 100

FIZBUZ  := $(shell seq 15 15 $(MAXVAL))
FIZ     := $(filter-out $(FIZBUZ),$(shell seq 3 3 $(MAXVAL)))
BUZ     := $(filter-out $(FIZBUZ),$(shell seq 5 5 $(MAXVAL)))

all:    $(shell seq 1 1 $(MAXVAL))

$(FIZ):
	@echo 'Fizz'

$(BUZ):
	@echo 'Buzz'

$(FIZBUZ):
	@echo 'FizzBuzz'

.DEFAULT:
	@echo $@

組み込み関数のfilterやfilter-outを使うと、テキストを空白区切りのリストと見なして集合演算っぽいことができる*5。ここではfilter-outを使って3の倍数/5の倍数の集合からFizzBuzzと表示するものを取り除いている。

pmake版の場合は少し工夫が必要だ。

# fizzbuzz6.mak
# Ubuntu 8.04.4 LTS (server) NetBSD make (pmake) 1.111-1build1
MAXVAL  ?= 100

FIZBUZ  != seq 15 15 $(MAXVAL)
FIZ     := ${:!seq 3 3 $(MAXVAL)!:C/${FIZBUZ:tW:S/ /|/g}//}
BUZ     := ${:!seq 5 5 $(MAXVAL)!:C/${FIZBUZ:tW:S/ /|/g}//}

all:    ${:!seq 1 1 $(MAXVAL)!}

$(FIZBUZ):
	@echo 'FizzBuzz'

$(BUZ):
	@echo 'Buzz'

$(FIZ):
	@echo 'Fizz'

.DEFAULT:
	@echo $@

gmakeのfilter-outに相当する機能がないので、代わりに正規表現を使用して3の倍数/5の倍数の集合からFizzBuzzと表示するものを取り除くようにしてみた。

修正版を書くにあたってオライリーの『GNU Make 第3版』を少しだけ読んだのだけど、便利な機能や興味深い機能が結構あることに気づいた。これは面白い。何かgmake縛りで遊んでみたくなる。

pmakeも興味深いことは興味深いのだけど、日本語の情報が少ないのが残念だ。

*1:Linuxでmakeと打つと大抵はgmakeが動く。pmakeはオプション扱いだし、大抵はpmakeと打たないと動かない。

*2:まあ最近はLLVMらしいけど。

*3:Linuxとは逆に、makeと打つとpmakeが動く。gmakeの方がオプションで、大抵はgmakeと打たないと動かない。

*4:でもMakefileをすらすら書けるというのもバッドノウハウに浸りきっているみたいで微妙かも。

*5:但しワイルドカードに注意。