ブライアン・カーニハンは写経したか?

ちょっと前の記事だけと、少し気になったので書いておく。

要約するのが面倒なので、内容については上記記事を参照してもらうとして……以下、上記記事を読んでいるという前提で、色々と省きながら書く。

上記記事では、プログラミングの上達にて効果的な手段として「写経」を挙げている。といっても、単なる写経ではなく:

そして筆者自身も、新しいことを勉強するのに、サンプルプログラムをとってきて改造するのをやめて、サンプルプログラムを一度印刷し、一言一句、上から下まで写経すると、驚くほどコードへの理解が深まることに気づきました。

なぜかというと、写経というのは、「なにも考えなくて良い」と言われるものの、人間ですから考えてしまいます。しかも、一言一句、記号のひとつひとつまで、意味を噛み締めながら入力していきます。

すると、サラッとサンプルプログラムを眺めているだけでは気づかなかった細かい法則や作法、サンプルプログラムを制作した製作者の意図などといったものがどことなくつかめてくるようになるのです。

もちろん、最初は意味がわからないなあ、と思って写しても構いません。

それでも、打ち込んでいくと、打ち間違いをしても、「あれ、これは少し変だぞ」と気づくようになります。

「さっきまではこの順番で丸括弧が来ていたのに、ここだけカギ括弧になっていて変だぞ」などということに、本能的に気づくのです。

https://wirelesswire.jp/2016/04/52633/

――正直なところ、個人的見解だが、これを「写経」といってよいものか疑問に思うところだが……。

ここで思い出したのが、K&RのKであるブライアン・カーニハンが『言語設計者たちが考えること (THEORY/IN/PRACTICE)』にて自身のプログラミング言語学習法について述べていたくだり(以下、同書初版第1刷のP.126より):

■言語を新たに学ぶ際には、どのようにするのですか?

Brian:言語を新たに学ぶには、行いたいことと近い内容の例を探し出し、それを使って学習していくのが最も近道になると考えています。例をコピーし、ニーズに合うよう変更した後、それを動作させることで知識を広げていくのです。このようにしてさまざまな言語を触っていると、頭の中が混乱し、ある言語から他の言語に頭を切り替える際に時間がかかるようになります。思い出そうとしている言語が大昔に学んだC言語と似ていない場合、特に時間がかかります。このため、優れたコンパイラが誤った構造や怪しげな構造の警告を出してくれるのは、学習する上でありがたいものとなります。こういった点でC++Javaといった強い型システムを持つ言語も学習に役立ちますし、標準への厳格な準拠を強制するオプションも良いものと言えます。

さて、先のブログ記事での学習法(仮に「写経」式学習法とでも呼ぼう)とカーニハンの学習法(こちらはカーニハン式学習法と呼ぶことにする)は、対立しているのだろうか? 私には、若干のスタンスの違いはあれど、どちらも全く同じことをやっているようにしか思えない(効率の差は別として……)。

そもそも「ソースコードを読み、内容を理解する」とは、どういうことだろうか?

プログラムは2つの姿をもつ。それは「静的構造」と「動的構造」だ。静的構造はソースコードであり、ソースコードによって表現された「プログラムの構造」だ。一方の動的構造は、プログラム実行時の実際の計算の進行(≒実行時の挙動、振る舞い)だ。

動的構造は、人間の目には見えない。プログラムの構造によっては、例えばデバッガを使ってステップ実行することで、動的構造に非常に近い環境を再現することが可能だ*1。だが現実のソフトウェアでは、例えばマルチスレッド・マルチプロセス・割り込みなどに起因する「タイミングの問題」など、その方法では再現できない動的構造がある。何よりもソフトウェアの大きさが、動的構造の全て(≒全ての分岐パス)を理解することを許さない。

「プログラムを理解する」とは、最終的には「プログラムの動的構造を高い精度で推測できるようになる」ということだ。

つまり、「ソースコードを読み、内容を理解する」とは、「ソースコードという静的構造を理解した上で、静的構造から動的構造を高い精度で推測できるようになる」ということを意味する。

(余談だが、「ヒトの頭では『大規模プログラム』の動的構造を全て理解するのは無理だから、静的構造と動的構造が極力一致するようにして、静的構造(≒ソースコード)から動的構造(≒プログラムの動作)が分かるようにしようぜ」というのが、構造化プログラミングにおけるGOTO文をめぐる話の核心だ*2

静的構造から動的構造を推測するためには、まず、静的構造を正しく理解できなくてはならない。つまり、ソースコードという記号の羅列を、意味のあるものとして解釈できなくてはならない。その上で、静的構造が動的構造にどのようにマッピングされるのか、判断できるようにならなくてはならない。

このように書くと、「ソースコードを読み、内容を理解する」という作業は、あたかも「まず静的構造を理解し、次に動的構造を推測する」という風にキレイに順序化されるものであるかのように見える。

しかし実際のところ、「静的構造の理解」と「動的構造の推測」は、混在した作業だ。静的構造の理解は、使用している言語・ライブラリ・フレームワークについての土地勘がないと、途端に難易度が跳ね上がる。また、静的構造だけ*3から動的構造を推測することも難しい。現実には、ある初見のプログラムを理解しようとする時、ソースコードを読む作業だけでなく、「ソースコードを読むための技術」で述べられているように動的解析*4を併用することが多い。そして興味深いことに、動的解析の結果を通じて、言語・ライブラリ・フレームワークのリファレンスからはいまいち分からなかった挙動が判明し、静的構造の理解が曖昧だった部分が正しく理解できるようになることがある。

私見だが、「写経」式学習法は「静的構造の理解」先行型のスタイルだ。一方のカーニハン式学習法は、動的解析をやや優先するスタイルだ。スタイルの違いはあるが、しかしどちらも「静的構造の理解」と動的解析を併用している。「写経」式学習法では、ディープラーニングの技術セミナーでの例のように、「写経、プログラム実行、ソースコード解説」のような順序で進めている。また、カーニハンが「静的構造の理解」を決して疎かにしてはいないことは、先のインタビューの後半にてコンパイラの警告や強い型システムについて言及していることから推測できる。

個人的に、両者のスタンスの違いがどこから生じているのか、なんとも妄想をかき立てられるものだ。

というのも、カーニハン式学習法のようなアプローチは、例えばかつてAT&Tベル研究所で採用されていた形跡があるのだ。以下、『UNIXカーネルの設計』の序文より:

本書の内容と構成は1983年から1984年にかけてAT&Tベル研究所での教育コース用に準備した資料から生まれてきた。コースはソースプログラムを読むことを目的としていたのだが、ここで、ひとたびシステムの概念とアルゴリズムが理解されているとプログラムの理解は簡単になるという事実を痛感した。UNIXの簡潔さと美しさを少しでも反映するように、アルゴリズムの記述はできるだけ簡潔にした。したがって、本書はシステムの内容の各行を解説したものではなく、それぞれのアルゴリズムの全体的な流れを説明したものである。

UNIXカーネルの設計』はカーネル本なので、背景というか前提条件は大分違うのだが、しかしUNIXカーネルソースコードを読むにあたり、動的解析の代わりに「大まかな全体の流れ」の解説を優先させた上で、その後に「ソースコードを読んで理解する」の段階に進んでいる。(公正な比較ではないことを承知した上で書くが)「写経」式学習法とは逆のアプローチだ。

私は、両者のスタンスの違いは、歴史的経緯によるものが大きいと考えている。

  1. 「便利で手軽で高品質なデバッガ」の普及による、実装スタイルの変化
  2. 「単純で十分に理解されている部品を組み合わせた」時代の終焉
  3. より安全な高水準言語の普及
  4. 宣言的な記述(抽象的な記述)の増加

1番目は、簡単にデバッガを使える環境が整ったことや、そのような環境しか知らないプログラマが増加したことで、実装時に「静的構造の美しさ」とでもいう部分に注力する割合が変化したのではないか、その結果として静的構造を理解する能力にレベル差が生じているのではないか、という仮説だ。現在のアプリケーション・ソフトウェア開発では、ソースレベル・デバッガの存在は当たり前であるし、大抵は統合開発環境に組み込まれている。デバッガの品質は高く、デバッガが再現する動的構造と実際の動的構造の差異は非常に小さい。このような環境では、デバッグ実行で動作を検証することが非常に容易だ。だから、自身が構築した静的構造に少々微妙な点があったとしても、デバッグ実行で確かめてしまえばよい。しかしデバッグ環境が整っていない場合、プログラムに問題が起きた際に即座に頼れるものはソースコードだけだ。だから、実装の際には静的構造の妥当性や美しさといった要素――すなわち「ソースコードをキレイに記述すること」に注力することで、予めバグを作りこまないように予防すると同時に、後で不具合報告が届いた際にソースコードのみから「静的構造の理解」と「動的構造の推測」を容易に行えるように準備するようになる。これら両者の違いが、プログラムの静的構造に対する姿勢(≒どの程度重視するか?)に影響を与えているのではないか?

2番目は「本の虫: MITがSICPを教えなくなった理由」で述べられている内容と同じだ。かつては、シンプルな言語仕様とスリムな標準ライブラリを用いていた。そのため、開発に用いる言語とライブラリは自分自身の手足も同然だった。この時代を経ている人は、ライブラリをつっつき回す時代になってからプログラミングを学び始めた人よりも、言語・ライブラリ・フレームワーク・その他ツールを体に覚えさせようとする傾向にあるのではないか、その結果知らず知らずのうちに静的構造を注視するようになっているのではないか?

3番目は、2番目とも関係している。例えば私はC言語が主戦場な人なのだが、C言語使いにとって「プログラムが動作し、意図した結果が得られる」という事実は気休めにもならない。Cプログラマは「それは本当に正しいプログラムか? 偶然、それっぽくいい感じに動いているだけではないか?」と常に疑いを抱く――たとえコードの見た目が正しそうで、問題なく動作しているステートメントでも、警告オプションを高くしてコンパイルした時に警告されないか確かめるし、警告が出なかった場合でも「言語仕様上の問題はないか? 単にこのコンパイラで警告が出なかっただけではないか?」と心配するものだ(だから言語仕様上の微妙な問題に詳しくなるし、コンパイラの「標準への厳格な準拠を強制するオプション」の有無を気にする)。それほど注意深くないと、自身の足を撃ちぬいてしまう。そういった、モダンな言語よりも危険で落とし穴のある言語の経験が長ければ長いほど、ソースコードを注意深く観察する癖が付いているのではないか? 安全な言語を使う機会が増えたことで、観察力が少し落ちているのではないか?

そして4番目。言語・ライブラリ・フレームワークの高水準化が進み、以前よりも宣言的・抽象的な記述がなされたコードが増えている。実のところ、宣言的な記述から実際の挙動や動作を理解することは難しい(≒理解に必要な知識が多いので、理解できるようになるまでのハードルが上がっている)。非常に乱暴な言い方だが、Java入門文書の当初にて「public static void main(String[] args)」を「呪文」として扱うことに似たスタンスで見慣れぬシンタックスに相対して、「詳細は分からないが、とりあえず目的とした結果を得ている」という水準で済ます癖が付いている人が増えているのではないか? 1〜3番目とハードウェア・リソースの向上により、それでなんとかなってしまうケースが増えているのではないか?

言語やライブラリが「手足も同然」から「ままならぬ代物」と化し、コードの記述がより抽象化された昨今では、「静的構造の理解」の難易度が上昇している。かつての、動的解析をやや優先するスタイルにおける「静的構造の理解」への取り組みは、実は徒手空拳も同然であり、今となってはなにかしらの武器が必要なのかもしれない。

また、かつては静的構造を注視し、ソースコードを注意深く観察するタイプだった人でも、1〜4に挙げた環境の変化より、静的構造に取り組む姿勢が軟化しつつある可能性はあるだろう。

そう考えると、「写経」式学習法のように「静的構造の理解」を先行させるスタイルでのプログラミング学習は、現状に合致したものなのだろうか。

*1:実のところデバッガは、プログラムの動的構造を巧妙に再現するツールにすぎない(デバッグ実行中の動的構造は、プログラム本来の動的構造ではない)。この辺は、古い本だが『デバッガの理論と実装 (ASCII SOFTWARE SCIENCE Language)』を参照。

*2:さらに余談だが、「静的構造も、大きすぎると理解できなくなるから、そこんとこ工夫でカバーしようぜ」というが、構造化プログラミングにおける分割統治とトップダウン手法の話だ。プログラミング言語の上に抽象層を構築することで、プログラムの各層の大きさを「ヒトの頭で理解できる大きさ」に抑えられるようにすることと、うまく抽象化することで「本当に必要になるまで上下階層のコードを読まなくても済む」という状態にすることの重要性を説いている。

*3:強調に注意。

*4:例えばデバッガ上で動かしてみるとか、printf(3)(≒トレースログ)を仕込んで動かすとか。