広まらなかった最高のアイデア

1984年、Donald Knuthは根本的な転換を提案する論文を発表した。プログラムはコンパイラのために書き、人間のために注釈をつけるべきではない。プログラムは人間のための文学として書かれ、そこからコンパイラが実行可能な部分を抽出すべきだ。彼はこれをliterate programmingと呼び、TeXをこの手法で構築した。

このアイデアは美しい。そして、現代のソフトウェア開発においてほとんど完全に存在しない。

TeXは、これまでに書かれた中で最も信頼でき、最もよく理解されている大規模プログラムの一つであり続けている。この成功は、Knuthがそれをliterate programとして書いたことに部分的に起因している。したがって、literate programmingが機能するかどうかという問いではない。明らかに機能する。問いは、なぜそれがKnuthには機能したのに、ほとんど他の誰にも機能しなかったのかということだ。

Literate Programmingが実際に意味するもの

KnuthのWEBシステム、後のC言語用CWEBは、単一のソースファイルを2つの異なる出力に分割することで機能する。あなたは、プログラムが何をするのか、なぜそれをするのか、そして各部分がどう組み合わさるのかを説明する物語のドキュメントを書く。その物語の中にコードの断片が埋め込まれている。

tangleツールはコードの断片を抽出し、コンパイラ用に順序付けして、マシンが実行できるファイルを生成する。weaveツールは散文を抽出し、コードの断片をきれいに印刷されたブロックとして整形し、人間が読めるドキュメントを生成する。

小さなCWEBプログラムが実際にどのように見えるか、以下に示す:

@* Introduction. This program computes the sum of a list of integers.
The algorithm is straightforward: iterate through the array and accumulate.

@c
#include <stdio.h>

@<Function prototypes@>;
@<Main program@>;

@*2 The core logic.
We define |sum_array| to take a pointer and a length, returning the total.

@<Function definitions@>=
int sum_array(const int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

@*2 Entry point.
A small driver to exercise the function.

@<Main program@>=
int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    size_t len = sizeof(data) / sizeof(data[0]);
    printf("%d\n", sum_array(data, len));
    return 0;
}

何が起こっているか注意してほしい。コードはコンパイル順ではなく、物語の論理によって構成されている。説明が先に来る。実装は、読者が準備ができたときに続く。@<Main program@>構文は名前付きチャンクであり、ドキュメントのどこにでも定義でき、tangleによってコンパイラ用の正しい順序に組み立てられる。

これが核心的な着想だ:人間はトップダウンの説明を読む。コンパイラはボトムアップの依存関係グラフを得る。両方とも好む形式を得る。

なぜKnuthには機能したのか

Knuthは典型的なプログラマーではない。彼は、偶然プログラムを書く数学者であり作家だ。彼はキーボードに触れる前に、数か月間紙の上でアルゴリズムを設計する。彼の書いた本は数十年後も読まれている。Knuthがコードは文学であるべきだと言うとき、彼は実際の文学を意味しており、それを実現するスキルを持っている。

TeXもまた完璧な候補だった。それは安定した仕様を持つバッチプログラムだ。特に行分割とページ分割ルーチンを中心としたアルゴリズムの核心は、広範な数学的説明から莫大な利益を得る。コードはゆっくりと変化する。ドキュメントは実装を生き延びる。

ほとんどのソフトウェアはこのようではない。ほとんどのソフトウェアは毎日変化する。仕様は設計されるのではなく発見される。対象読者はアルゴリズムを理解しようとする読者ではない。スタンドアップの前にバグを修正しようとする開発者だ。

編集の問題

Literate programmingは執筆プロセスを仮定している。ほとんどのプログラミングは編集プロセスだ。

エッセイを書くとき、あなたは草稿を書き、修正し、磨き上げる。構造は計画されている。ソフトウェアを書くとき、あなたは探索し、テストし、リファクタリングし、出荷する。構造は浮かび上がる。Literate programのドキュメントは、後から塗りつけるコーティングではない。それは主要な構造だ。変数名を変更したり、関数を抽出したり、論理を並べ替えるたびに、あなたは文学を書き直している。

これは悪循環を生む。散文がコードと密接に結合していれば、リファクタリングは高くつく。散文が疎結合であれば、コードからずれて間違ったものになる。どちらにせよ、ドキュメントは腐敗する。

現代のバージョン管理はこれを悪化させる。Literate programは単一の物語ドキュメントだ。Pull requestはソースツリーに対するdiffだ。コードレビューツールはmain.cの行を理解する。main.wの物語的チャンクは理解しない。Syntax highlightingから静的解析、CIパイプラインに至るまで、ツールのエコシステムはコンパイル可能なソースファイルを期待している。Literate programmingは、これらすべてからオプトアウトすることを要求する。

スキルのミスマッチ

Knuthは、プログラマーも作家だと仮定した。ほとんどの人は違う。

優れた技術文書は珍しい。なぜなら難しいからだ。それは、あなたが理解していることをまだ理解していない読者への共感を必要とする。何をしたのかではなく、なぜそうしたのかを説明する規律を必要とする。コーディングとは別のスキルである編集を必要とする。

チームにliterate programsを書くよう求めるとき、あなたは彼らにKnuthになってほしいと求めている。意図を説明し、代替案を探り、読者を論理を通して導く広範な散文を求めている。平均的なコードコメントは// TODO: fix thisだ。それと文学との間の隔たりはツールの問題ではない。人間の問題だ。

これが、より優れたツールでliterate programmingを復活させようとする試みが一貫して失敗した理由だ。ボトルネックは決してweaveとtangleのツールチェーンではなかった。ボトルネックは、ほとんどのプログラマーがエッセーを書きたくなく、ほとんどのコードベースがエッセー品質のドキュメントを報わないことだ。

実際にそれを置き換えたもの

業界はKnuthの目標を放棄しなかった。業界は、異なる手段を通じてそれのより弱いバージョンを達成した。

型システムは、かつて段落の説明を必要としていた意図をエンコードするようになった。関数がList<T>の代わりにNonEmptyList<T>を受け入れるとき、type checkerはドキュメントが記述できたに過ぎない保証を強制する。Rustのborrow checkerが参照のエスケープを拒否するとき、それは散文では数ページを要する制約を伝える。

DocstringsやJavadoc、Rustdoc、TypeDocのようなAPIドキュメントツールは中間地帯を作り出した。それらは、ドキュメントが構造を主導することを要求せずに、ドキュメントをコードの隣に保つ。ソースか生成されたドキュメントのどちらかを読むことができ、両方は同じファイルに存在するため同期したままだ。

テストスイートは実行可能なドキュメントになった。よく書かれたテストは「この入力が与えられたら、この出力を期待する」と、散文が決してできないより正確に言う。Property-based testは不変条件をエンコードする。Snapshot testは意図を捉える。テストはすべてのコミットで実行されるため、散文のようにずれることはない。

おそらく最も重要なことに、プログラミング言語そのものがよりliterateになった。Pythonは疑似コードのように読める。Rustのif let構文はpattern matchingの意図を直接表現する。コードが説明であるとき、コードの別の説明は必要ない。

実際に機能した現代版

Knuthのビジョンがほぼそのまま生き延びた場所が一つある:computational notebooksだ。

Jupyter notebooks、R Markdown、Observableは、散文とコードが単一のドキュメントで共存することを可能にする。散文が説明する。コードが実行する。出力がインラインで表示される。これは名前以外のすべてにおいてliterate programmingだ。

NotebooksはWEBが失敗したところで成功した。なぜなら文脈が異なるからだ。データセットを探索するデータサイエンティストは、物語的プロセスに従事している。コードはゆっくりと変化する。対象読者は、意思決定を理解しようとする人間の読者だ。編集サイクルはリファクタリングではなく探索だ。ソフトウェア工学でliterate programmingを殺した制約は、データサイエンスでは特徴となる。

教訓はliterate programmingが間違っていたということではない。このアイデアには狭い生態的地位があるということだ。プログラムが研究対象となる完成品であるとき、それは繁栄する。プログラムが変更されるべき生きたシステムであるとき、それは窒息する。

そこから得るべきもの

おそらく、次のサービスにCWEBを採用することはないだろう。それで構わない。しかし、Knuthの根本的な洞察を適用する価値は依然としてある。

物事に、それらが自らを説明するような名前をつけよう。process_dataという名前の関数は、WEBに触れたことがなくてもliterate programmingの失敗だ。remove_expired_sessions_older_thanという名前の関数は、それ自身のドキュメントを持っている。

あなたが持っていたらよかったと思う説明を書こう。コードのあるセクションが理解するために散文の段落を必要とするなら、その段落を書こう。コメント、docstring、または設計ドキュメントに入れよう。メディアは説明する行為ほど重要ではない。

役立つときは、物語の構造をコンパイラの構造から分離しよう。README-driven development、アーキテクチャ決定記録、RFCはすべて、ツールチェーンと戦うことなく人間向けの物語を書く方法だ。

Knuthは私たちに本のようにプログラムを書いてほしかった。私たちは結局、プログラムについての本を書いた。それは彼が望んでいたより弱い勝利だが、それでも勝利だ。