ビルドツールに関数型プログラミングが向いている

世の中にビルドツールは数あれどどれも気に食わない。いっそのこと自分で作っちまおうかとすら思い、ビルドツールに必要な機能を考えている。

ビルドツールは手続きを書くのが主流だが、関数型プログラミングを意識して作った方が上手く作れるのではないか。

ビルドするときに要る情報は次の3つしかない:

  • 欲しいもの
  • 欲しいものを作るのに必要なもの
  • 必要なものから欲しいものを作る手順

必要なものがファイルとしてすでに存在しない場合は、必要なものを欲しいものとした3要素が新たに要求される。

欲しいものと必要なものの関係、つまり依存関係は有向非巡回グラフ(DAG)になる。DAGをたどっていくのがビルドシステムである。トポロジカルソートでDAGをたどる順番を決定して、実行していくということをどのビルドシステムも行っていると思われる。

3要素はすべて関数にできる。手順は関数自体だ。必要なものは引数でこれは関数として与えられる。ビルドするべきファイルも、ファイルの中身を出力する関数とみなされる。欲しい物は関数の値となる。

最終成果物を得る関数を評価したら、遅延評価されて手順が最初から実行されていくという形になるだろう。トポロジカルソートが要らなくなる。

Makeは欲しいものがすでに存在していて、必要なもののタイムスタンプが欲しいものより新しくなかったら、欲しい物をビルドし直すということをしない。これを実現する場合はメモ化のような動作をさせることで簡単に実現できる。

関数型プログラミングっぽくビルドツールをつくると何がよいのかというと、予め関数を用意しておけばビルドスクリプトがシンプルになりそうなことだ。イメージ的には、*.cコンパイルしてa.outをつくるなら、以下で済むようになって欲しい。

target = file('a.out')
file('a.out') = compile(file('*.c'))

targetというのがゴールの関数であり、ビルド実行時にはこれを評価する。fileというのはファイルを示す関数で、ワイルドカードも適応できる。compileはコンパイルをする関数で、ファイルの種別からデフォルトの動作が規定されている。

あるいはJavaをビルドしてテストするなら、

target = test(file('foo.jar'), file('foo-test.jar'))
file('foo.jar') = compile('src/main/java')
file('foo-test.jar') = compile('src/test/java')

みたいな感じだ。

必要に応じて関数を置き換えれば柔軟な操作もできるだろう。

target = file('a.out')
compile = compile(CFLAG='-O2 -Wall')
file('a.out') = compile(file('*.c'))

あんまりまとまっていないが、私が欲しいのはこういう感じのイメージである。

こういうのないのだろうか。

GNU Make 第3版

GNU Make 第3版