「悪い方が良い」原則と僕の体験談

ソフトウェアの世界には「悪い方が良い」原則という有名なエッセイがある。キレイにレイヤ分けされた一貫性のある良いデザインよりも、一見手抜きの悪いデザインのほうが実は良いときもあるという話だ。この逆説的なデザイン原則を僕は身をもって体験したことがある。それについてちょっと書いてみようと思う。

僕はlldというリンカの現行バージョンのオリジナル作者だ。リンカというのはコンパイラと組み合わせて使うもので、実行ファイルやDLLを作るのに使用される。lldはプロダクトとしてはかなり成功していて、標準のシステムリンカとして採用しているOSがいくつかあったり、GoogleやFacebookなど皆が知っているような大規模サイトの中で広く使われていたりする。

現在のlldは2世代目で、第1世代のlldは僕がプロジェクトに参加する前から存在していたのだけど、数年前にそれを捨てて一から書き直すということになった。lld v1は、「悪い方が良い」エッセイの言葉を借りるなら、MIT/Stanfordスタイル(良いデザイン)で書かれていて、lld v2はニュージャージースタイル(悪いデザイン)で書かれている。どうして「良いデザイン」を捨てて「悪いデザイン」で書き直したのか? それを理解するためにまずlld v1のデザインを見てみよう。

lld v1は次のような方針に従って作られていた。

1. JITや他の言語の組み込み向けにフレキシブルな機能を提供するために、リンカの機能を提供するライブラリ(フレームワーク)を作る。リンカコマンドldはライブラリの使用例の一つという位置づけにする。

2. プラットフォーム依存のコードを最小にするために、プラットフォーム非依存の中間表現を決めて、リンカの大部分のコードはその中間表現を扱う。

3. リンカはプログラムとして頑強でなければならない。当然クラッシュしてはいけないし、見つけたエラーはライブラリの呼び出し元に伝えなければいけない。

この設計方針は最初は妥当なものに思えた。実際には、プロジェクトにフルタイムで参加して18ヶ月が過ぎても、僕はまだlldをきちんと走らせることにひどく苦労していた。ほとんどのプログラムは一応リンクできるものの、すごく時間がかかるし、ちょっとした機能を足すのでさえとても大変で、なにかが間違っていることは明らかだった。

問題は、どうやら設計がややこしすぎたことだった。それぞれの項目について何が問題なのかを見てみると次のようになる。

1. そもそも「ライブラリとしてのリンカ」を本当に必要としている人はいるのだろうか? 僕の結論はnoだった。それが欲しいと口にする人はわりとたくさんいたけれど、誰も、コマンドではなくライブラリでなければいけない具体的な例を示すことはできなかった。

ライブラリかコマンドかという話は、実際的な利便性ではなく信念についての話みたいな面が多分にあった。ある種の人たちに言わせれば、モノリシックなコマンドは時代遅れで、フレキシブルなライブラリとして作ることが当然正しいやり方であり、僕のような「あえて間違ったやり方をしようとする人たち」を黙って見過ごすわけにはいかないというのだ。この信念には一定の裏付けもあった。lldの親プロジェクトであるLLVMはコンパイラのプロジェクトで、コンパイラは比較的キレイにレイヤリング可能なプログラムなので、コンパイラばかり書いている人たちにとってはそれこそが普遍的に正しいデザインに思えたのだろう。

2. プラットフォーム非依存の中間表現は実際にはうまく機能しなかった。というのも、プラットフォーム依存の各種機能がファイルフォーマットだけではなく具体的なリンカの挙動にも影響を与えているので、中間表現はすべての機能を表現できる必要があり、結果的に中間表現はすべてのファイルフォーマットのすべての機能を持つことになってしまった。共通部分をくくりだすのではなく、逆に全部入りの恐ろしい中間レイヤができてしまったのだ。

こうなるとプログラミングはすごく難しくなってしまった。ライブラリとして考えた場合、Windowsリンカの機能XとUnixリンカの機能Yを同時に使うといったことができるのだけど、そのときに全体としてどう振る舞うべきかというのは当然前例がなかったし、多くの場合自明でもなかった。僕らはいろいろ議論して、そういう組み合わせに対して意味をなすセマンティクスを注意深く定義して、すべてのプラットフォームで正しく動く複雑なコードを時間をかけて書いていったのだけど、実際にはこれはまったく無駄な作業だった。そういう組み合わせを実際に使いたい人なんて誰もいなかったからだ。それどころかlld v1は本当のユーザは恐らく1人もいなかった。

また、数GiBあるような入力をすべて中間表現に変換するのはとても時間のかかる処理で、そのためにプログラムが遅くなってしまっていた。

3. プログラムの頑健さについては、入力の一貫性を一つ一つチェックするためにはかなりの量の追加のコードを書く必要があったし、それによりプログラムが遅くなってしまっていた。数GiBの入力をすべてチェックするのは当然時間がかかるのだ。

結局、僕らは(というか僕は)次のような「悪いスタイル」でプログラムをスクラッチから書き直すことにした。

1. lld v2は単にコマンドとして書くことにした。プログラムのエントリポイントは一つだけで、ライブラリみたいにいろんな機能を自由に組み合わせて使うということはできないようにした。これによりプログラムが劇的に簡単になった。新機能がほしいときは、ソースを直接いじってその機能を足すことになるけど、それでよい、ということにした。ソースコードが短くてわかりやすくなったことにより、新機能追加はずっと簡単になった。

2. lld v2では、中間表現は使わないで、プラットフォーム依存のネイティブのファイルフォーマットをそのまま扱うことにした。lld v2ではWindows版、macOS版、Unix版でデザインは共有しているけど、ソースコードは共有していない。したがって各ターゲットごとにかなり似たようなコードを書くことになる。これは素人目にわかるレベルで明らかに悪いデザインに思えるだろうけど、実際には、あらゆる部分で微妙に異なる機能を無理に統合して書くのに比べて、遥かにコードが簡素化された。

3. プログラムの頑強さについては積極的に一線を引くことにした。コマンドの間違った使い方などは当然ユーザに報告しないといけないけど、入力ファイルのバイナリが壊れている場合などは、単にクラッシュしてよいことにした。実際の僕の方針はある意味もうちょっと過激で、入力を信頼していないコードは自分では書かないし、他人のコードでも入力をいちいちチェックしているものは、たとえ完璧に動くとしてもコードレビューで阻止することにした。

リンカの入力ファイルはコンパイラによって作られているので、ファイルフォーマットのレベルで壊れているファイルがリンカに渡ってくることはまずありえなくて、実際のところこれによりクラッシュが増えたりすることはなかった。入力を信頼していないコードと比べて、入力を信頼しているコードはずっと単純で、実装が大幅に簡素化された。

これらのデザイン変更の利点はすぐに明らかになった。lld v2を書き始めてから数週間である程度のプログラムがリンクできるようになり、しかもその時点ですでにv1より10倍くらい速くて、既存の最速のリンカと比べても2倍は速かった。数カ月後には実用的なプログラムがリンクできるようになった。この書き直しの成功を見て他のオープンソース開発者が開発に参加してくれるようになった。ついに僕らは正しいニーズを把握したのだ。ユーザにとってリンカというのはきちんと動いて速ければそれでよいものであり、僕は単にきちんと動いてすごく速いというものを作った。

数年後の今では、lldは驚くほど多くのOSやプロジェクト、企業で使われている。Unix全般での開発環境の標準ツールになるという、当初は野心的すぎるように思えた目標を達成することも、今では可能な範囲に入ってきていると思う。

lld v2の成功のすべての原因が「悪いデザイン」だったとまでは言わないけど、それが大きな要因になっていたのは間違いない。だから「悪い方が良い」原則をここで再度強調してみてもよいだろう。「賢いやり方」や〇〇原則といったものにこだわりすぎないこと。実装の単純さはとてもとても重要で、そのためにはレイヤ分けの一貫性や完全性、コード重複の少なさを、リーズナブルな範囲で犠牲にしてもよい。自分が良いと思うデザインで小さくて実際に動くものを作り、それを次第に育てていくことが大切だ。

(追記: もともとの「悪いほうが良い」エッセイの最初のバージョンは1989年に書かれた。その中では「良いデザイン」はMITやStanfordのスタイルとして言及されている。面白いことに僕はこのデザイン原則のエッセイをStanfordのコンピュータサイエンスの授業で読まされた。どうやらStanfordの授業もこの30年間ですっかり「悪い方向」に変わってしまったようだ。その授業で僕がクラスの掲示板に投稿した文章を多少修正の上で翻訳したのがこのエッセイである。)

(追記2: プログラミングに関するTuring Complete FMというポッドキャストやってます。)

この記事が気に入ったらサポートをしてみませんか?