GNU Makeで集合演算

GNU Makeはテキストを「空白区切りの単語の集合」として扱うことが比較的容易だ。組み込み関数*1を使うと簡単な集合演算を簡単に実現できる。

集合を作る

単純に空白区切りで単語を並べれば、GNU Makeではそれが集合となる。ただ組み込み関数を使う都合上、集合の要素には英数文字のみの単語を使ったほうが無難だろう。

実験用に適当な集合を作りたい場合は seq(1) が便利だ。ここでは重複しない30以下の正の整数を集合の要素として扱う。

MAXVAL ?= 30
SET_A  := $(shell seq 3 3 $(MAXVAL))
SET_B  := $(shell seq 5 5 $(MAXVAL))

3の倍数と5の倍数。どこかで見たことのあるような……。

積集合、共通部分(論理積

集合Aの中から集合Bにも含まれる要素を取り出したい場合、組み込み関数filterを使う。

$(filter $(SET_B),$(SET_A))

引数の順番に注意。上記の例からAとBを逆にすると「集合Bの中から集合Aにも含まれる要素を取り出す」という意味になる。もっともどちらも答えは変わらない。

和集合(論理和

集合Aと集合Bの要素を取り出す(但し重複を取り除く)場合は組み込み関数sortを使う。

$(sort $(SET_A) $(SET_B))

引数のAとBの間がコンマではなく空白であることに注意。

組み込み関数sortは各要素に対してシェルスクリプトでいうところの、

sort | uniq

的な処理を行った結果を返す。ただ辞書順にソートするので、各要素が数値の場合に残念なことになる。でもまあ気にしない方向で。

差集合

集合Aから集合Bにも含まれる要素を取り除いた結果を取得したい場合、組み込み関数filter-outを使う。

$(filter-out $(SET_B),$(SET_A))

正確には「集合Aの中から、集合Bに含まれる要素以外を取り出す」という意味になる。

対称差(排他的論理和

集合Aと集合Bの対称差を求める方法は2通り。

ひとつはAとBの和集合X及び積集合Yを求めた上で、XとYの差集合を求める(XからYを取り除く)。

$(filter-out $(filter $(SET_B),$(SET_A)),$(sort $(SET_A) $(SET_B)))

もうひとつは差集合同士の和集合を求める。

$(sort $(filter-out $(SET_B),$(SET_A)) $(filter-out $(SET_A),$(SET_B)))

どちらの方法も積集合、和集合、差集合の演算の組み合わせだ。もっとエレガントな方法は無いだろうか?

テスト

ここまでの方法が正しそうか、実際にMakefileを書いて確かめた。

# setop_proto.mak - sample of set operations
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

MAXVAL ?= 30
SET_A  := $(shell seq 3 3 $(MAXVAL))
SET_B  := $(shell seq 5 5 $(MAXVAL))

.PHONY: all
all:
	@echo 'set A    : $(SET_A)'
	@echo 'set B    : $(SET_B)'
	@echo 'A and B  : $(filter $(SET_B),$(SET_A))'
	@echo 'A or B   : $(sort $(SET_A) $(SET_B))'
	@echo 'A and !B : $(filter-out $(SET_B),$(SET_A))'
	@echo 'A xor B  : $(filter-out $(filter $(SET_B),$(SET_A)),$(sort $(SET_A) $(SET_B)))'
	@echo 'A xor B  : $(sort $(filter-out $(SET_B),$(SET_A)) $(filter-out $(SET_A),$(SET_B)))'

Ubuntu 8.04.4 LTS サーバ版のGNU Make 3.81で実行した結果。

$ make -f setop_proto.mak
set A    : 3 6 9 12 15 18 21 24 27 30
set B    : 5 10 15 20 25 30
A and B  : 15 30
A or B   : 10 12 15 18 20 21 24 25 27 3 30 5 6 9
A and !B : 3 6 9 12 18 21 24 27
A xor B  : 10 12 18 20 21 24 25 27 3 5 6 9
A xor B  : 10 12 18 20 21 24 25 27 3 5 6 9
$ _

Windows XP上でMSYS付属のGNU Make 3.81で実行しても同じだった。

ライブラリ化

GNU Makeにはユーザ定義関数(マクロ)の機能があるので、ここまでの集合演算を関数化してライブラリっぽくしてみた。

# setop.mak - set operations library ver.1
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

# $(call setop-and,A,B) - A and B
define setop-and
$(filter $2,$1)
endef

# $(call setop-or,A,B) - A or B
define setop-or
$(sort $1 $2)
endef

# $(call setop-and-not,A,B) - A and !B (relative complement)
define setop-and-not
$(filter-out $2,$1)
endef

# $(call setop-xor,A,B) - A xor B
define setop-xor1
$(filter-out $(call setop-and,$1,$2),$(call setop-or,$1,$2))
endef
define setop-xor2
$(sort $(call setop-and-not,$1,$2) $(call setop-and-not,$2,$1))
endef

組み込み関数にandやorが存在する*2ので、名前がかぶらないように `setop-' というプレフィックスを付けている。また展開時に先頭に余分な空白が付かないように、関数定義の中身にわざとインデントを付けていない。

今回定義した関数はどれも1行で済むこともあり、再帰変数(再帰展開変数)を使ってこんな風に定義することもできる。

# setop.mak - set operations library ver.2
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

# $(call setop-and,A,B) - A and B
setop-and   = $(filter $2,$1)

# $(call setop-or,A,B) - A or B
setop-or    = $(sort $1 $2)

# $(call setop-and-not,A,B) - A and !B (relative complement)
setop-and-not   = $(filter-out $2,$1)

# $(call setop-xor,A,B) - A xor B
setop-xor1  = $(filter-out $(call setop-and,$1,$2),$(call setop-or,$1,$2))
setop-xor2  = $(sort $(call setop-and-not,$1,$2) $(call setop-and-not,$2,$1))

こちらの方がスッキリした見た目だ。

上記の関数群はsetop.makという独立したMakefileに記述している。使いたいときはsetop.makをインクルードすればよい。

# setop_sample.mak - sample of set operations library
# Ubuntu 8.04.4 LTS (server) GNU Make 3.81-3build1

include setop.mak

MAXVAL ?= 30
SET_A  := $(shell seq 3 3 $(MAXVAL))
SET_B  := $(shell seq 5 5 $(MAXVAL))

.PHONY: all
all:
	@echo 'set A    : $(SET_A)'
	@echo 'set B    : $(SET_B)'
	@echo 'A and B  : $(call setop-and,$(SET_A),$(SET_B))'
	@echo 'A or B   : $(call setop-or,$(SET_A),$(SET_B))'
	@echo 'A and !B : $(call setop-and-not,$(SET_A),$(SET_B))'
	@echo 'A xor B  : $(call setop-xor1,$(SET_A),$(SET_B))'
	@echo 'A xor B  : $(call setop-xor2,$(SET_A),$(SET_B))'

多重インクルードとかはどうなっているのか不明。

実行結果はこんな感じ。

$ make -f setop_sample.mak
set A    : 3 6 9 12 15 18 21 24 27 30
set B    : 5 10 15 20 25 30
A and B  : 15 30
A or B   : 10 12 15 18 20 21 24 25 27 3 30 5 6 9
A and !B : 3 6 9 12 18 21 24 27
A xor B  : 10 12 18 20 21 24 25 27 3 5 6 9
A xor B  : 10 12 18 20 21 24 25 27 3 5 6 9
$ _

まとめ

いやぁ、GNU Makeって本当に興味深いプログラミング言語ですねぇ。「オレのGNU Makeは宇宙だ」と言わんばかり。

想像がつくように、参考書はコレ。

GNU Make 第3版

GNU Make 第3版

オライリーから『プログラミングGNU Make』とか何とかが出版されたら絶対買うと思う。

このエントリについて

id:eel3:20110924:1316791928 にGNU Make版FizzBuzzの完成バージョンを追加していて思いついたネタ。反省も後悔もしていないけど、NetBSD make(pmake)での実現方法が考えつかなかった点が心残りだ。

*1:GNU Makeには関数(というかマクロ)がある。ちょっと調べた限りNetBSD make(pmake)には無いようだ。

*2:但し条件判定というか実行制御の関数で、ドキュメントを見た限りLispのandやorに近いようだ。