なぜLispなのか?― “データ” と “コード”

たくさんの人から私が昨日Hacker Newsに書いたコメントについてもっと詳しく説明してほしいというメッセージをいただきました。例えば以下のような質問です。

Lispは単なる表記法の1つにすぎないと私は考えますが、間違っているでしょうか? Lispのコードをデータ構造にマッチさせるのがなぜそんなに重要なことなのか理解できません。(おそらく、そのマッチさせるという行為がなぜLispを使うのかという答えになるのだとは思いますが。)私はマクロの大事な何かを見落としているでしょうか?  何か私が気付いていないことがあるでしょうか?

この質問に答えるには少し長くなりそうだったので、ブログに投稿することにしました。以下が私の答えです。

手短に言えば、Lispは単なる表記法の1つではなく、プログラミングとは何かという考え方を根本的に覆すものなのです。プログラミングに関する主流な考え方は、「プログラミングとは、データという人工物に対して、それを処理するスタンドアローンな人工物であるプログラムを生み出すことからなる」というものです。もちろんプログラムはデータであるということは誰しもが知っている事実ですが、上記の主流な考え方は2つのコンセプトの人為的な区別を主張するものです。そうです、プログラムはデータですが、コンパイラという名の特殊なプログラムのためだけに存在するデータということになります。コンパイラは書くのが難しく、勉強をする必要があります。ただし、自分自身でコンパイラを書く人は(アカデミックな演習をする以外では)少ないでしょう。大抵の人は、そんなおもちゃのようなコンパイラではなく、熟練された技術を有する専門家が書いたコンパイラを使用します。

Lispのプログラミングは、マシンとのより一般的なインタラクションです。マシンに何をして欲しいかを記述するという行為は、あなたが記述したことのマシンによる実行、その結果の観察、その観察を元に加えられた「あなたがマシンにしてほしいこと」の記述の変更と交互します。つまり、どこまでやればプログラムが完成するとか、どこでそれがスタンドアローンな人工物になるという明確なラインはありません。しかし、Lispを使えばそのような明確なラインを引くことができますし、スタンドアローンな実行ファイルを生成することができます。これはC言語でインタラクティブなプログラムを書くことができるのと同じようなものです。しかし、LispはAIのリサーチを目的として開発されたので、意図的にインタラクティブに作られています。一方でC言語はプログラムのオペレーションを目的に開発されたので、インタラクティブではありません。Lispと違ってC言語にとってスタンドアローンな実行ファイルを作成することがネイティブであるように、C言語と違ってLispにとってはインタラクティブ性はネイティブです。

もちろん反復する以外に選択肢がない場合もあるでしょう。時には完成されたデザインを生成するのに必要な知識が十分でなかったり、実験を行わなければならなかったり、スピードが命の時もあるでしょう。このような場合には小さなプログラムを組み合わせて大きなプログラムにするような一般的な仕組みが便利です。C言語の世界ではそのような仕組みが存在します。それがpipeです。しかし、C言語は階層的なデータをシリアライズ/デシリアライズをする標準的な方法を持ち合わせていません。その代わりC言語は、様々な種類のシリアライゼーションフォーマットを持っています。Fixed-Width、delimiter-separated、MIME、JSON、ICAL、SGML とそこから派生したもの、HTML 、XMLなどです。これでもごく一部です。これらは単なる、データのシリアライゼーションフォーマットにすぎません。コードの記述に使うプログラミング言語はそれぞれ独自のシンタックスと特異性を保持しています。

C言語の生態系はシンタックスが重要であるという奇妙な考え方を生み出しました。シンタックスのデザインにはたくさんのエネルギーが注がれていますし、LEXやYACCなどのツールが広く使われています。C言語の世界では、パーサを書くことがプログラマの仕事の大半を占めます。

今も昔もC言語に携わる人なら、コードを表現するのにシリライズフォーマットのデータの1つを使ってみようという考えに一度は至るでしょう。しかしその努力はすぐに終わりを迎えます。XMLやJSONで表現されたコードは、コードを表現するためにデザインされたシンタックスを使って表現されたコードに比べると、絶対的にひどいものになるからです。結局、データとしてコードを表現するのは得策ではなかったと気付き、パーサを書くことになるのです。

しかし、それは間違いです。

XMLやJSONで表現されたコードがひどいものになるのは、データとしてコードを表現するのが得策ではなかったということではなく、XMLとJSONがシリアライズフォーマットしてうまくデザインされていないからです。つまり、句読点が多すぎるのです。XMLの場合は余計なものが多すぎます。Lispが他のシンタックスと違ってコードを表現するのに優れているのは、S式のシンタックスがシリアライズフォーマットとしてよくデザインされているからです。S式は最小限のコードで書くことができます。3つを比べると以下のようになります。

XML: <list><item>abc</item><item>pqr</item><item>xyz</item></list>

JSON: ['abc', 'pqr', 'xyz'] 

S式: (abc pqr xyz)

XMLはこんなにシンプルな例でも明らかにひどく長ったらしくなってしまいました。JSONとS式はあまり変わらないようにも見えますが、よく考えてください。以下のような場合、S式は本領を発揮します。

(for x in foo collect (f x))

同じものをJSONで書くと以下のようになります。

['for', 'x', 'in', 'foo', 'collect', ['f', 'x']]

これをXMLにするとどうなるかは、みなさんの宿題にします。

ただ式を眺めるのではなく、実際にタイプしてみると、その違いがより明確になります。(ぜひ試してみてください。)小規模なデータ構造には害がなさそうに見える引用符やカンマは、非常に複雑なデータ構造にとっては即座に耐えがたい負担となるのです(そして、XMLのようにSGMLから派生した言語では、完全にお手上げです)。

Lispが非常に素晴らしく効果的な理由は、Lispを使う人がコードをデータとして表現しようとする直感が実際に正しいからです。 Lispこそ非常に有効な手段です。特に、Lispを使うと、インタプリタやコンパイラを書くのが本当に簡単になります。そして、C言語の世界でパーサを書くのが通常の仕事であるのと同じくらい、Lispのプログラミングにとって、新しい言語を生み出し、そのためにインタプリタやコンパイラを書くことが当たり前になります。しかし、簡単にインタプリタやコンパイラを書くためには、コードとデータを表現する正しい構文から始める必要があります。つまり、コードとデータを表現する最小構文から始めるのです。それ以外のものから始めると、大量のカンマや引用符、山括弧で訳が分からなくなってしまうでしょう。

そうなると、まずはS式から始める必要があります。S式こそが階層データを表現する最小構文だからです。 考えてみてください。階層データを表現するために必要なのは、トークンセパレータとブロックデリミタという2つの構文要素のみです。S式では、スペースがトークンセパレータで、丸括弧がブロックデリミタになります。それだけです。他のやり方では、ここまで構文要素を減らすことはできません。

Lispで丸括弧がよく目につくからといって、Lispが他のプログラミング言語に比べて丸括弧を多用しているわけではないということは特筆に値するでしょう。Lispには1つしかブロックデリミタ(丸括弧)がないため、丸括弧が目につきやすくなるのです。その他の言語は、区切られるブロックの種類によってさまざまなブロックデリミタを使っています。例えば、C言語ファミリは引数リストや部分式に丸括弧、配列に角括弧、コードブロックや辞書には波括弧を使います。また、カンマやセミコロンをブロックデリミタとして使用します。両者を比較してみると、大抵Lispの方がCのような言語よりもブロックデリミタが少ないです。特にコールバックがいたるところにあるJavaScriptでは、デリミタが頻繁にあらわれ、よく深みにはまります。そして、文脈によって異なる正しいデリミタを把握するのは、プログラマにとって頭を悩ませる問題です。Lispを使うプログラマは、そんな心配をする必要がありません。ブロックを閉じる場合は、閉じ括弧をタイプするだけです。常に頭を悩ます必要がないので、Lispを使うプログラマは思考力に余裕があり、本当に自分たちが解決したい問題に集中することができます。

そのことに関しては、コーディングに立ち戻った方がよさそうですね。もちろん何度も繰り返しです。