このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

プログラミング言語Makeの関数型フラグメント:まとめとサンプル

一時Makefileに凝っていました。Makefileの記述構文のなかに、「Lispに似た小さな関数型言語が埋め込まれているんだ」とか言ってましたよね。あれはまー、遊び半分なんだけど、このテの知識は、実際にMakefileを読み書きするときにも役にたちます。

過去のMakefile関係エントリーを読み返そうと思ったら、けっこう量があるし、紆余曲折があるんで、ここに整理してまとめておこうと思います。

内容:

  1. 参考資料
  2. 概念と用語
  3. データ型
  4. 変数
  5. 関数
  6. 制御構造
  7. 画面出力と強制終了

●参考資料

Makefileに関しては、次のエントリー群でゴタゴタと説明しています。

  1. Makefileの書き方、その勘どころ
  2. Makefileの書き方:プログラミング言語Make
  3. プログラミング言語Make 補遺:引数付きの関数
  4. Make 補遺の補遺:遊んでいるだけ
  5. Make言語で算術演算 <-- バカ!

もっとも信頼できる原典は:

●概念と用語

ここでは、GNU makeのMakefile構文の一部を関数型言語と考えます。よって、プログラミング言語の用語法を使うことにします。

ここで使う用語 普通使われている用語
Make言語 Makefileの記述構文
言語処理系 GNU makeコマンド
ソースコード Makefile
変数 単純変数
関数 再帰的変数
関数の引数 再帰的変数のパラメータ
代入 := による代入
関数定義 = による代入

ソースコードの拡張子は.mkとします。次がソースコードと処理系(インタプリタ)実行の例です。Make言語の処理系(GNU makeのことね)はバージョン3.81以上を使用してください。


# hello.mk
$(info Hello, world.)

end:
@echo Done.


$ make -f hello.mk
Hello, world.
Done.

$

以下では、Lispと親和性が高い $(...) という構文を使いますが、${...} も使えます。なお、個々の組み込み関数については説明しないので、マニュアルなどにあたってください。

●データ型

Make言語のデータは基本的にテキスト文字列(ストリング)です。しかし、文脈により文字列がワードリスト、またはブール値(boolean)として解釈されます。

  • ワード : 空白を含まない文字の並び
  • リスト : いくつかのワードを空白で区切って並べたもの
  • ブール値 : 空文字列が偽、その他は真

次の例では、変数peopleの値は、foreach繰り返し構文のなかではリストとして解釈されます。foreachに関しては後でまた取り上げますから、雰囲気だけつかんでください。


# helloEverybody.mk
people := Taro Hanako Akira Yoko
$(foreach person,$(people),$(info Hello, $(person).))

end:
@echo Done.


$ make -f helloEverybody.mk
Hello, Taro.
Hello, Hanako.
Hello, Akira.
Hello, Yoko.
Done.

$

次の例では、変数peopleの値は、if条件式のなかではブール値して解釈され、関数strip, substのなかでは単なる文字列と解釈されています。


# helloEverybody-2.mk
NIL :=
SPACE :=$(NIL) $(NIL)
COMMA :=,

# ARGの値はコマンドラインから設定する。

people := $(ARG)

people := $(if $(people),$(strip $(people)),nobody)
people := $(subst $(SPACE),$(COMMA) ,$(people))

$(info Hello, $(people).)

end:
@echo Done.


$ make -f helloEverybody-2.mk
Hello, nobody.
Done.

$ make ARG='Taro Hanako ' -f helloEverybody-2.mk
Hello, Taro, Hanako.
Done.

$

●変数

  1. 変数名 := 式 により変数の宣言と初期化がされる。
  2. 変数名 := により変数に空文字列が設定される。
  3. $(変数名) により変数参照ができる。
  4. 宣言されてない(未定義)変数の値は空文字列とみなされる。未定義変数と値がほんとに空文字列である変数の区別はorigin関数で行える(下の例)。
  5. コマンドライン指定の変数や環境変数は宣言なしに参照できる。これらの変数は、値を書き換えるべきではない(コマンドラインからの変数はそもそも書き換えできない)。
  6. 変数名 += 式 で古い値への追加(アペンド)、変数名 ?= 式 で変数が未定義のときだけ代入ができる。「+=」を関数定義(後述)に使うべきではない。


# vars.mk
foo :=
$(info foo='$(foo)')
$(info bar='$(bar)')
$(info baz='$(baz)')

$(info origin(foo) is $(origin foo))
$(info origin(bar) is $(origin bar))
$(info origin(baz) is $(origin baz))

end:
@echo Done.


$ make baz= -f vars.mk
foo=''
bar=''
baz=''
origin(foo) is file
origin(bar) is undefined
origin(baz) is command line
Done.

$

●関数

  1. 関数名 = 式 により関数を定義できる。
  2. 関数への引数は、関数定義の右辺内で $(1), $(2), ... などで示す。
  3. $(関数名) により引数なし関数を呼び出せる。
  4. $(関数名 引数1,引数2,...) により組み込み関数を呼び出せる。
  5. $(call 関数名,引数1,引数2,...) によりユーザー定義関数を呼び出せる。残念ながら、組み込み関数と同じ構文でユーザー定義関数を呼び出すことはできない。*1

次は、関数型言語でよく使う制御構造(高階関数)であるfoldrを定義して、foldrを使ってリストを逆順にする関数reverseを定義したものです。


# reverse.mk

Null = $(if $(1),,T)
Cdr = $(wordlist 2,$(words $(1)),$(1))

Foldr = \
$(if $(call Null,$(3)),$(2),$(call $(1),$(firstword $(3)),$(call Foldr,$(1),$(2),$(call Cdr,$(3)))))

PutLast = $(2) $(1)

Reverse = $(call Foldr,PutLast,,$(1))

FRUITS := apple orange banana
$(info FRUITS=$(FRUITS))
$(info Reverse(FRUITS)=$(call Reverse,$(FRUITS)))

end:
@echo Done.


$ make -f reverse.mk
FRUITS=apple orange banana
Reverse(FRUITS)= banana orange apple
Done.

$

上記のMake言語ソースを忠実にJavaScript(処理系はRhino)に翻訳すると次のようになります。ただし、Make言語のリストをJavaScriptの配列として解釈しています。


/* listは配列だとする */

/* Null = $(if $(1),,T) */
function Null(list) {
if (list.length) {
return false;
} else {
return true;
}
}

/* Cdr = $(wordlist 2,$(words $(1)),$(1)) */
function Cdr(list) {
return list.slice(1, list.length);
}

/*
* Foldr = \
* $(if $(call Null,$(3)),
* $(2),
* $(call $(1),$(firstword $(3)),
* $(call Foldr,$(1),$(2),$(call Cdr,$(3)))))
*/
function Foldr(fun, acc, list) {
if (Null(list)) {
return acc;
} else {
return fun(list[0], Foldr(fun, acc, Cdr(list)));
}
}

/* PutLast = $(2) $(1) */
function PutLast(item, list) {
list.push(item);
return list;
}

/* Reverse = $(call Foldr,PutLast,,$(1)) */
function Reverse(list) {
return Foldr(PutLast, [], list);
}

/* FRUITS := apple orange banana */
var FRUITS = ["apple", "orange", "banana"];

/* $(info FRUITS=$(FRUITS)) */
print("FRUITS=" + FRUITS);

/* $(info Reverse(FRUITS)=$(call Reverse,$(FRUITS))) */
print("Reverse(FRUITS)=" + Reverse(FRUITS));

●制御構造

  1. 条件分岐は $(if 条件式,式)、または $(if 条件式,式1,式2)
  2. 繰り返しは $(foreach 変数名,リスト,式)。リストの各要素が変数名で指定した変数に代入され、式が実行される。式のなかでは代入された変数値(各項目)を使用できる。全ての評価結果を空白をはさんで連結したリスト文字列が値となる(下の例を参照)。
  3. 短絡的に実行される(ショートサーキット評価される)andとorも制御構造として使える。構文は、$(and 式1[,式2,…])$(or 式1[,式2,…])。andは、引数に偽(空文字列)が出現したところで評価が終わり、空文字列が返される。orは、引数に真(非空文字列)が出現したところで評価が終わり、その非空文字列が返される。


# helloEverybody-3.mk
people := Taro Hanako Akira Yoko
greeting := $(foreach person,$(people),Hello, $(person).)
$(info $(greeting))

end:
@echo Done.


$ make -f helloEverybody-3.mk
Hello, Taro. Hello, Hanako. Hello, Akira. Hello, Yoko.
Done.

$


# eq.mk
Eq = $(and $(findstring $(1)X,$(2)X),$(findstring $(2)X,$(1)X))

$(info '$(ARG1)' and '$(ARG2)':$(if $(call Eq,$(ARG1),$(ARG2)),Equal,NOT Equal))

end:
@echo Done.


$ make ARG1=foo ARG2=foo -f eq.mk
'foo' and 'foo':Equal
Done.

$ make ARG1=foo ARG2=bar -f eq.mk
'foo' and 'bar':NOT Equal
Done.

$

●画面出力と強制終了

  1. $(info 式) で式の評価結果を画面に書き出せる。$(info 式)全体は空文字列に評価される。
  2. $(error 式) で式の評価結果をエラーメッセージとして画面に表示し、処理系が終了する。


# deadOrAlive.mk
Eq = $(and $(findstring $(1)X,$(2)X),$(findstring $(2)X,$(1)X))

$(if $(call Eq,$(ARG),Die),$(error Dead),$(info Alive))

end:
@echo Done.


$ make ARG=Hello -f deadOrAlive.mk
Alive
Done.

$ make ARG=Die -f deadOrAlive.mk
deadOrAlive.mk:4: *** Dead. Stop.

$

*1:ユーザー定義関数が組み込み関数と同じように呼び出せれば、劇的に可読性が向上するでしょうに、ほんとに残念。