makeのくびき

「gulpって何だよ、makeでいいじゃん(要約」論争について、私もちょっと一本講釈をぶってみることにする。あれやこれやといった実利的な話をするつもりはない。そういうものは既に書いた人がいるのでそちらを参照のこと。

Gruntの思い出

Gruntは、私の印象で言えば車輪の再発明の失敗作のようなもので、タスク間の依存関係が破滅への一途をたどり管理不能に至るなど、宣言型の負の側面が強く出てしまった。しかし、設定は本当にサンプルコードのコピペだけで組み立てられるので、JSが不得手なデザイナーなどには非常に受けが良かったという点は忘れてはならない。ちょうど、html5ブームが本格化して, Apache Antとかに慣れ親しんだJava(主にSIer)系の人が入ってきたタイミングにあった道具かつ、Yeomanファミリーにも組み込まれており、それでいて簡単な事をやらせるには悪くはない具合のシンプルさ、そして設定ファイルがホスト言語で書けるという、「悪くない」塩梅だったために流行ったというのが私の感想。あ、ファイル監視によるタスクの自動実行機能いわゆるwatchも流行った一因だろう。ただ、まあ、GruntであればMakefileを書くのと大差ないと言われても仕方が無い、むしろmakeの方がまだマシだと言われても仕方が無いようには思う。私もそこについては割と同意(というか、私はGruntfile書くなら俺はMakefile書くぜ!な人間だったので、まんまそういう意見だった)。

gulpたん

Gruntがそこそこ使われるようになってきて、「これって色々やり始めるとイマイチじゃね?」という認識がアーリーアダプターの間で醸成され始めた矢先に出たのがgulpだったように思う。これは中々にmodernな着想に立っており「ビルドプロセスとは、ファイルストリームに対してtransform関数を適用してmapを行うものだ」という前提に立っている(意訳)。パイプラインの各タスクの単位を明確にmap関数に切り分けたので、処理パイプラインJavaScriptのfunctionとして明瞭になった上、streamとしてオンメモリでの処理が行われるようになったため、tempファイルを作るなどのアプローチに頼っていたGruntに対する速度的な優位も生まれた(そして明らかに初期は「Gr**tに比べて速い」を売り物にしていた記憶がある)。gulp流のStream哲学に合わないサードパーティのtask pluginをブラックリスト認定するなどの行動にも出るなど開発陣は極めて急進的な側面もあるが、思想という点では中々にmodernな路線に進化する事になった。

けれども、gulpには問題点があった(と認識している)。せっかくプログラマブルかつ関数をタスクの単位としたのに、肝心のタスクとなるmap関数を作ろうとすると、gulpのstreamに併せて変換メソッドを隠す為にdirtyな関数を作り、クロージャとしてタスクを返すスタイルが、処理モデル上、強制されるので、その局所的に発生した汚物を隔離する為にtask pluginの形式でパッケージ化する必要が出てしまった。これはGrunt文化を引きずっている面もあるだろう。結果、「関数を組み合わせる事が出来る = 本来的には各種ツールチェインがビルドシステム構築用に提供しているAPIをそのまま使う」という事ができずに、隠蔽の向こう側に一段置いてから使う羽目になってしまった。本家のトランスパイラではなく、task pluginのアップデートに私のビルドチェーンが左右される日々の始まりである。peer dependency指定なら良かったね? いやいや、peer dependencyだってsemver的にmajor upがかかったら対応待ちだぜ?

理想的なgulpに必要だったのは、そこらへんの普通のトランスパイラのAPIをgulp streamに対応したtransform関数に変えるビルトインファクトリだったのかもしれない。これがあったならgulpはhigh modernなビルドシステムなり得たに違いない!(と私は勝手に思っている)。もしくは、Rxのような、もっと別物の抽象機構に乗っかっておくべきだったか。いずれにせよ、これらは夢物語である。

もうすこし汎用ビルドシステムの四方山話は続く。JavaScriptの変換に限ると、asterと呼ばれるツールが登場した。「お前らfile streamとか言うけど、jsファイルをやり取りしてるじゃねえか。毎回パースするの無駄だからASTを直接流そうぜ」というアプローチ。コンパイラとしては非常に正しいツッコミ。だが、これはイマイチ市民権を得られていない。考えられる理由は、シンプルに言ってgulpを上回る魅力が無いからであろう。コンパイラ的に正しいと言っても、それはアーキテクチャが綺麗というだけで、これで変換したコードからバグが消える訳でもない。毎回パースを行い抽象構文木に変換する処理は確かに計算機資源の無駄だが、そんなものはハードウェアの進歩で解決している。GruntではディスクI/Oがボトルネックだったが、gulpではstreamスタイルを強制した事で解決「させた」。というかそこが重要ではないのは, Grunt、gulpともにデフォルトでインクリメンタルビルドのためのツールチェインを備えていないことから明らかであろう。変更監視と都度フルビルドの体力勝負で賢いビルドが悩みがちな問題を解決してしまったのだ。それと、エコシステムの問題。いくら「フロントエンドWeb」が浮気性だからと言っても、ビルドツールという彼女を何回も乗り換えるほどの好色はそこまでいない。おまけに各種変換ツールチェインはES3~5のコードこそを中間表現として、そこに対して独自のASTを生成し独自の変換技法を尽くしているのが当たり前になった今更になって、各自の手の内を標準化しようなどと言うのはいささか困難な話であろう。

と、いうのがgulp一派に対する弁護と批判と多くの私情を交えた説明。

コマンドラインが一級市民の国

で、TypeScriptとかbrowserifyとかbabelとかfacebookのreact-toolsの話に戻ると、彼らはあくまでもコマンドラインからの使用を一般的な利用におけるfirst class citizenに取り扱う。一応、謎ビルドシステム・謎IDEからも遊べるようにnpm packageとしてのプログラマブルな利用も可能だぜ、というスタンスに近い。gulp pluginを公式にメンテしたりもするが、あくまでもtier 2であり、公式でなく野良協賛者が出してるgulp pluginともなると、公式も薦めこそするが「こんなのもあるから使ってね!」みたいな扱いに近いように思う。

なので、件の記事で話題になっているTypeScriptやbrowserifyについては、そもそもgulpとの相性が悪い道具なので、「makeの方が良いじゃん!」と言われたら、「そうですね」と頷くより他は無かったりする。

makeじゃダメなの

結局、makeじゃダメなん?という疑問については、私はダメじゃないと思うけど、なんだかんだで.PHONYとか書かなきゃいけないのだし、謎オプションを調べるのとgulpのタスクをコピペするのは特に大差がないし、むしろプラガブルな設計してるものをコピペする方が楽だし、だいたいプロジェクト始めるときに1回調べるだけなのは一緒じゃね?(後は使い回しになるよね?)と思う。

gulpの利点としてプログラマブルであることが挙げられる点についても、「そもそもビルドツールに条件分岐などのプログラマブル性を求めるべきではない」という声はあるものの、そうは言っても人類は、今までAutotoolsとか、その他メタビルドシステムを(makeをバイトコードと看做す形式で)何度か発明してきてしまったわけで、いずれはどこかでプログラマブルな需要を吸収しなければならないので今更な問題のように思う。まあ、程度問題として全部meta-metaにやられても困るので、どの程度までプログラマブルを許容するdesignにするかという問題はあるのは同意だけど、それはもっとミクロな個別の問題だと思いたい。

そして、プログラマブル性を求め始めると、なんだかんだで完全ないし高水準なスクリプティング機構が欲しくなるので、ホスト言語で書けるように作りたくなるのは仕方ないんじゃないかなとは思う。特にコンパイラのオプションとかは、3つで済む筈が無くて、それをコマンドライン1行で書くのは地獄だし、幾つもオプションを作るのも冗長なので、スクリプタブルなフラグで管理したいという気持ちは尤もであろう。

それにJavaScriptの場合、Cとかのような原始的なincludeベースのビルドシステムじゃなくて、コンパイラがエントリーポイントさえ分かれば後は依存関係を全部掌握してしまうモデル(というかコンパイラが明示的に情報を出さないと取れない)ので、GNU Makeの機能をフルに使ったインクリメンタルコンパイルなんてものは期待できない。結局、.PHONY buildとか書かないと、エントリーポイント以外のファイルを弄った場合にビルドできなかったりしたり(もちろん私のmake力が足りない可能性は十分にありますよ、ええ)。

ともかく、なので「Makefileで完結するじゃん!」なんてことはそんなに無くて、共通インターフェースのコマンドディスパッチャとしてMakefileを書きつつ、実態はgulpなり好きな物に投げろと言う感じになる。ここらへんはMozillamachとかを連想してもらえればわかりやすいと思います。

この手の議論をするときに捨て置かれがちな話に、クロスプラットフォーム対応の話がある。ついつい世の中のソフトウェアエンジニアは * nixでしか仕事しない前提で話を進めてしまったりするのだけど、特にWebエンジニアはOSXLinuxしか使ってない前提で話してしまうのだけど、世の中そんなことはなくて(当たり前だよね)、Webフロントエンド(Web制作)の場合、エクセル入稿やワード入稿とかがあったりするので、Winで開発してる人がそれなりに居たりする。そういうのが無くても、Windowsで開発していたりする。別にIE対応がどうとかじゃなくて。そういう人にとっては、cmd.exe叩いても動かない可能性が非常に高いmakeよりも、node.jsさえ動けばとりあえず動くGruntとかgulpの方が遥かに簡単だったりする。黒い画面は恐ろしいのだ。少なくともホスト言語の環境が動けば、とりあえず動くというのは非常に強みで、Gruntがなぜあそこまで流行ったのかは、クロスプラットフォーム耐性の高さとコピペビリティの高さだと言っていいと思う。もし、あれが*nixでしか動かなかったら、ああは流行らなかっただろう。みんなbuild.shかMakefileかbuild.batを書いている。おそらく今でも。

そういう諸々の状況を考えると、「makeでいいじゃん」というのは頭では納得がいくし、確かに壮絶に車輪の再発明を回させられている趣はあるのだが、なんだかんだで再発明された車輪を回した方が便利なケースも結構あるよねと感じるわけです。思想的には最も急進派であるgulpも、gulp pluginを使わずに自分でモジュール呼んだりchild_process.spawnしたものを呼び出すだけみたいなハイブリッド運用で回すといい具合に適材適所になったりする。

コピペでビルドツールを作るのか?というのは、確かに知力を武器として飯を食うエンジニア的には敗北感はある。が、落とし穴を掘りにくいdesignであるならばコピペビリティは正義である。我々は最強のビルドツール設定を書きたい訳ではなく、とにかくビルドチェーンを作りたいだけなので、コピペビリティ上等みたいなpragmaticぶりで良いのではないかと思う。いやあまあ、同僚がクソみたいなビルド設定作ってきたら、悪態をつきながらコードレビューで闘えば良いのではないかな。闘うのが面倒くさくなったら、コッソリ好きなスタイルに上手い事リファクタリングだ。

というわけで、なぜ人は再発明を繰り返すのか?という問いについては、一種の諦めと、ツールチェインの特性の問題と、「何十年も前に作られたCとUNIXの降霊方法を今更覚えたい若者はそんなに居なかった」という結論で良いんじゃないかと思います。C/C++Javaに次いで10年ぶりにビルドツールチェインを作る機会に恵まれたのだから、知恵は活かしつつ新しいものを使いたいのは人間の好奇心。コンパイルと言っても、C/C++Javaに比べればさしたる作業をしているわけではないのだし、コンパクトで調教しやすい物で良い。これで解決できないものが出来たら?今のJavaScriptの領分ではないので諦めてC++を書こう。GYPが君を待っている。個人的にはRust(のcargo)にも頑張ってほしい。

あ、余談だけど、この分野、たぶんgolangで依存関係を書けるコマンドディスパッチャを上手い事作れると、一発当てられそうな機運はあるなーと思った。既にあったかもしれない。ハードボイルドな燻し銀のgolangを使えば、おじさんも若者も大満足でwin-winなんじゃないかと。golangの特性的に妥当な感じもあるし。それがJavaScriptの文化に受け入れられるかは知らないけれど。