拡張可能なプログラミング言語

遠くない昔,Lispは何のためのプログラミング言語かと尋ねれば, 多くの人が「人工知能(AI)用のプログラミング言語」と答えただろう. 実際はLispとAIとの関係は歴史の偶然に過ぎない. Lispの開発者はJohn McCarthyで,彼は「人工知能」という言葉の提唱者でもある. 彼の生徒と同僚達は彼らのプログラムをLispで書いた. それがLispがAI用のプログラミング言語と言われ出したきっかけだ. このつながりは広く取り上げられ,1980年代の短いAIブームの間に 大変な程繰り返されたので,ほとんど迷信のようになってしまった.

幸運なことに,AIだけがLispの目的でないとの言葉が広まり始めた. 最近のハードウェアとソフトウェアの進歩のおかげで,Lispは商業的にも成功し始めた: 今では最高のUNIX系テキストエディタGNU Emacs, 業界標準のデスクトップCADソフトAutocad, そして先駆的ハイエンド出版ソフトInterleafで使われている. これらのプログラムでのLispの使われ方はAIとは何も関係ない.

LispがAI用プログラミング言語でないのなら,いったい何なのか? Lispを取り巻く仲間でそれを判断するのでなく,言語そのものを見てみよう. 他のプログラミング言語でできないことのうち,Lispには何ができるか? Lispの最も特徴的な性質の一つは,Lispによって書かれているプログラムに合わせて Lispを仕立てることができる点だ. LispそのものがLispのプログラムの一つであり, Lispのプログラムはリストとして表現できるが,リストはLispのデータ構造なのだ. これら2個の原則が相俟って, どのユーザも組み込みのものと区別の付かないオペレータをLispに追加できることになる.

進化によるデザイン

Lispではユーザに独自のオペレータを定義する自由があるので, Lispを必要なプログラミング言語にきっちり仕立てることができる. ユーザがテキストエディタのプログラムを書いているのなら, Lispをテキストエディタを書くための言語に変えることができる. またCADソフトのプログラムを書いているのなら, LispをCADソフトを書くための言語に変えることもできる. そしてどんなプログラムを書くかまだ確かでないなら,Lispで書いておくのが安全な賭けだ. それがどんな種類のプログラムになったとしても,それを書いている間に, Lispはその種類のプログラムを書くためのプログラミング言語に進化していることだろう.

どんなプログラムを書くかまだ確かでないなら? 人によってはこの文は奇妙に響くだろう. その人は (1)~これからやることを注意深く計画し,そして (2)~それを実行する,というモデルとの不愉快な対照を目にしたことになる. このモデルによれば,プログラムのすべき動作を決める前にプログラムを書くことを Lispが勧めるとすれば,それは的外れな考え方を勧めているに過ぎないことになる.

さて,全くそんなことはない. 「計画−実装」方式もダム建設や侵略作戦の決行にはいい方法だったが, 人々の経験によればプログラムを書くのにいい方法だったかどうかは定かでない. なぜか?~きっとそれは,コンピュータは大変正確だからだ. きっとプログラムにはダムや侵略作戦よりもヴァリエーションが多いからだ. または「余分」ということに関する古い概念とソフトウェア開発に類似点が ないせいで,古い方式が機能しないことによるのだろう: ダムが30\%余分なコンクリートを使っていても,それは間違いなのかどうかぎりぎりの所だ. しかしプログラムが30\%余分な動作をしていたら,それは間違いだ.

古い方式が失敗に終わるのがなぜかを言うのは難しいかもしれないが, それは確かに失敗し,結果は誰の目にも明らかだ. ソフトウェアが期日通りに完成したことがあるだろうか? 熟練プログラマは,どれ程慎重にプログラムの計画を立てても, プログラムを書き始めると,必ず計画にどこか不完全な点が見つかることを知っている. 計画が望みのない程まで間違っていることもある. しかし「計画−実装」方式の犠牲者のほとんどはその基礎の健全さを疑おうとはしない. 代わりに彼らは人間の失敗を責める: 「もう少しいい展望の下に計画を立ててさえいたら,こんな問題は全て避けられただろう.」 最高レベルのプログラマでさえ実装となれば問題に突き当たるのだから, 人々が必ずそれだけの展望を持つことを望むのはどうやら無茶のようだ. おそらく「計画−実装」方式は, 我々の持つ限界により適した別のアプローチで置き換えることができるだろう.

適切なツールがあるならば,プログラミングへの別のアプローチが可能だ. なぜ実装の前に計画を立てるのか? プロジェクトの立案にどっぷり浸かり込むことの大きな危険性は, 自らを困難に追い込んでしまう可能性がある点だ. もっと柔軟なプログラミング言語があれば,この心配を減らせるのでは? まさにその通りだ. Lispの柔軟性は全く新しいプログラミングのスタイルを生み出した. Lispにおいては,計画の大部分を立てるのはプログラムを書きながらでいい.

なぜ後知恵が浮かぶのを待つのか? Montaigneが気付いたように,考えを明確にするにはそれを書き下ろそうとすることが一番だ. 自らを困難に追い込んでしまうとの心配からひとたび解放されれば, この可能性を最大限に活用できる. プログラムを書きながら計画を立てる能力には二つの重要な結果につながる: まず,プログラムを書くのにかかる時間が短くなる. それは計画を立てると同時にプログラムを書くと, 注意を集中することで本当のプログラムが出来上がるからだ. そしてその方法で出来たプログラムはよりいいものであると分かるだろう. それはプログラムの最終デザインは必ず進化の産物だからだ. プログラムの目的を探す間,間違っていた部分を, 明らかになったその場で必ず書き直すという原則を守る限り, 最後に出来上がったものは, あらかじめ計画に数週間を費やした場合よりも優美なプログラムになるだろう.

Lispが多目的なプログラミング言語だからこそ, この種のプログラミングが実用的な代替手段になる. 事実,Lispの持つ最大の危険はLispがユーザに悪影響を与えるかもしれないことだ. 一度Lispをしばらく使うと,プログラミング言語とアプリケーションとの相性に敏感になりすぎ, 元々使っていたプログラミング言語に戻っても, これでは必要な柔軟性が手に入らないという思いに常に囚われるようになりかねない.

ボトムアップ・プログラミング

プログラムの機能的要素は余り大きくなるべきでないというのが, プログラミング・スタイルの長年の原則だ. プログラムの構成要素が,読めば理解できる状態を超えて肥大化するなら, それは複雑さの固まりに成り果て,(大都市が流れ者を隠すように)簡単にエラーを覆い隠す. そういうソフトウェアは読み辛く,テストし辛く,デバッグし辛い.

この原則に従い,大規模なプログラムは部品へと分割しなければいけない. またプログラムが大規模であればある程,さらに分割しなければいけない. プログラムを分割する方法とは何か? 伝統的アプローチはトップダウン・デザインと呼ばれる: 「このプログラムの目的はこれらの7個だから, プログラムを7個の主要サブルーチンに分割することにする. 1個目のサブルーチンはこれら4個のことを行わなければいけないから, それ自身のサブルーチンが今度は4個になり,...」といった具合だ. このプロセスはプログラム全体が適切なバラバラ状態 ---全ての部分が意味のある仕事を行えるだけの大きさでありながら, 単一のユニットとして理解できる位小さくなった状態--- になるまで続く.

熟練Lispプログラマがプログラムを分割する方法は違っている. トップダウン・デザインと同様,彼らには従う原則があり, それはボトムアップ・デザイン ---プログラミング言語を問題に適するように変えていく--- と呼ばれる. Lispでは,プログラムをただプログラミング言語に従って書くことはしない. プログラミング言語を自分の書くプログラムに向けて構築するのだ. プログラムを書いているとき, 「Lispに○△のオペレータがあればなあ.」と思うことがあるかもしれない. そうしたらそれを書けばいい. 後で,新しいオペレータの使用が プログラムの別の部分のデザインを簡潔にまとめることにつながったと気付くだろう. そういった感じでプログラミング言語とプログラムは共に進化する. 交戦中の2国間の国境のように,二つの境界は何度も書き直される. それが落ち着くのは, 山や川(ここではユーザの問題の持つ自然な境界)に辿り着いたときだ. 最終的には,ユーザのプログラムではプログラミング言語がそのために 設計されたかのような見掛けになる. そしてプログラミング言語とプログラムが相性良く落ち着いたとき, ユーザは明快で小規模で効率的なコードを手にする.

ボトムアップ・デザインは,同じプログラムをただ別の順番で 書くだけのことではないということは,強調する価値がある. ボトムアップで作業すると,大抵別のプログラムが出来上がる. 単一の一体となったプログラムではなく, 抽象的なオペレータを持つ大規模なプログラミング言語と, それで書かれた小規模なプログラムが出来るのだ. ユーザは敷石ではなく,高いアーチを手にする.

典型的なコードでは,単なる簿記に過ぎない部分を一度抽象化すると,残るものはずっと短い. プログラミング言語を高く構築すればする程,頂点からそこに至るまでの道のりは短くなる. これは幾つかの長所をもたらす:

  1. プログラミング言語に仕事を任せることで, ボトムアップ・デザインでは小規模で機敏なプログラムが生まれる. 短いプログラムは多数の構成要素に分割する必要がない. そして構成要素が少ないということは, プログラムが読み易く修正し易いものだということだ. また構成要素が少ないということは,構成要素同士の連結も少ないということなので, そこでエラーが起きる可能性も少ない. 工業デザイナが機械の可動部分を減らそうと努力するのと同様に, 熟練Lispプログラマはボトムアップ・デザインを使って プログラムの大きさと複雑さを減らそうとする.
  2. ボトムアップ・デザインはコードの再利用を促進する. プログラムを二つ以上書くとき, 最初のプログラムのために書いたユーティリティの多くは他のプログラムでも有用になる. ユーティリティの大規模な基盤を手にしてしまえば,新しいプログラムを書く手間は, Lispそのもので書かなければならないときにかかる手間の数分の一に過ぎない.
  3. ボトムアップデザインはプログラムを読み易くする. この種の抽象化のインスタンスは,読む人に汎用オペレータを理解するよう要求する. 機能的抽象化のインスタンスは, 読む人に特殊目的のサブルーチンを理解するよう要求する \footnote{「でもあんたのユーティリティを全部理解しないことには, プログラムが読めなくなるじゃないか.」 そういった言葉は大抵誤りだ.なぜかについては,第4.8節を参照.}.
  4. ユーザにコード内のパターンに常に関心を払うようにさせる. ボトムアップでの作業はプログラムのデザインに関するアイディアを 明確にするのに役立つ. 一つのプログラムの中で種類の違う2個の構成要素が形の上で似ているなら, 類似性に気付くよう促されるし, おそらく単純な方法でプログラムをデザインし直すよう促されるだろう.

ある程度までは,ボトムアップ・デザインはLisp以外のプログラミング言語でも可能だ. ライブラリ関数を見ればそこには必ずボトムアップ・デザインの例がある. しかしLispはこの点に関してずっと幅広い力を与え, プログラミング言語にそれに比例してLispのスタイルの中で 大きな役割を演じるよう促す ---その役割が余り大きいので, Lispはただのプログラミング言語の一つではなく, 全く違ったプログラミング方法になっている.

このスタイルの開発は,幾つかの小規模グループによって書ける程度のプログラムに 適しているというのは確かに真実だ. しかし同時に,このスタイルは小規模グループのなし得ることの限界を拡張する. Frederick Brooksが「人月の神話」 (The Mythical Man-Month) \note{myth}の中で示唆したのは,プログラマのグループの生産性は その規模に対して線形には増大しないということだった. グループのサイズが増大するにつれ,個々のプログラマの生産性は低下してゆく. Lispプログラミングの経験はこの法則を表現するためのより素敵な方法を提案する: グループのサイズが減少するにつれ,個々のプログラマの生産性は向上してゆく. 相対的に小規模なグループが勝利を収める. 理由は単純,小さいから. 小規模グループがLispによって可能になるテクニックを活用するとき, 完全な勝利が待っている.

拡張可能なソフトウェア

Lispスタイルのプログラミングは,ソフトウェアが複雑さを増すにつれ重要性を増してきた. 今時のユーザからのソフトウェアへの要求は余りに多く, プログラマにはほとんど予想しきれない. ユーザ自身も要求の全てを予想することができなくなっている. しかしユーザの望みを完璧に叶えるソフトウェアをプログラマが供給できなくとも, プログラマには拡張できるソフトウェアを供給することはできる. プログラマは自分のソフトウェアをただのプログラムからプログラミング言語に転換し, 技術の高いユーザは必要な付加的機能をその上に作り上げるようになる.

ボトムアップ・デザインは拡張可能なプログラムに自然につながっていく. ボトムアップ・プログラムの最も単純なものは,2層から成る: プログラミング言語とプログラムだ. 複雑なプログラムも一連の層として書き上げることができる. それぞれの層は1段下の層に対してプログラミング言語として機能する. この方針が最上層まで貫かれれば,その層がユーザの使うプログラミング言語となる. そのようなプログラムではあらゆるレベルで拡張が可能になっていて, 伝統的なブラックボックスとして書かれ,後から拡張性を付け加えたシステムよりも 遥かにいいプログラミング言語が出来上がることが多い.

X WindowsとTeX はこの原則に基づくプログラムの古い例だ. 1980年代には高性能のハードウェアによってLispを自らの拡張言語とする 新世代のプログラムが可能になった. その最初は有名なUNIXのテキストエディタ,GNU Emacsだ. 次はAutocadで,これはLispを拡張言語とする大規模商用製品としては初になる. 1991年にはInterleafの新ヴァージョンがリリースされたが, それはLispを拡張言語に使っていただけではなく,かなりの部分がLispで実装されていた.

Lispは拡張可能なプログラムを書くためには素晴らしくいいプログラミング言語だ. それはLisp自身が拡張可能なプログラムであるからだ. Lispの拡張性をユーザにも渡すようなLispプログラムを書けば, 実質的には何もせずに拡張言語が出来たことになる. そしてLispでLispプログラムを拡張することと, 伝統的なプログラミング言語でそれを行うこととの違いは, 誰かに直接会うことと手紙でやりとりすることとの違いのようなものだ. 外部プログラムへのアクセス手段を提供することだけで 拡張可能になったプログラムでは, 既定のチャンネルを通じて連絡し合う二つのブラックボックスがせいぜいだ. Lispでは,拡張機能は背後にあるプログラム全体に直接アクセスできる. これはユーザにプログラムのあらゆる部分へのアクセスを 与えなければならないということではなく, ただアクセスを与えるか与えないかの選択肢があるというだけのことだ.

このような部分的アクセスがインタラクティブな環境と結びつくと,最高の拡張性が得られる. 自分のプログラムの拡張機能の基礎として使えるようなプログラムは, どれもかなり大規模になりがちだ. おそらく規模が大きすぎて全体的なイメージが掴めない程だろう. 何か不確かな点があるときどうなるだろう? そのとき元のプログラムがLispで書かれていれば, それをインタラクティブに調べることができる: データ構造を調査できる.関数を呼び出せる.ソースコードを読むことさえできる. この種のフィードバックにより,確かな自信を持ってプログラムを製作できる. 思い切った拡張機能を書けるし,しかもそれが素早くできる. インタラクティブな環境は常にプログラミングを容易なものにしてくれるが, 拡張機能を書いているとき程それが有り難いときはない.

拡張可能なプログラムは諸刃の剣だが, 最近の経験によればユーザはただの剣より諸刃の剣の方を好む. どんな危険性が備わっていようとも, 拡張可能なプログラムの方が勝るようだ.

Lispの拡張

Lispに新しいオペレータを加えるには2通りの方法がある: 関数とマクロだ. Lispではユーザの定義した関数は組み込み関数と同じ地位を占める. もしmapcarの変種が新しく必要になったら,それを自分で定義し, mapcarを使うのと同じようにそれを使うことができる. 例えば,ある関数が1から10までの整数に適用されたときに それが返す値のリストが必要なら, 新しいリストを作ってそれをmapcarに渡すことができる:

(mapcar fn
        (do* ((x 1 (1+ x))
              (result (list x) (push x result)))
          ((= x 10) (nreverse result))))

しかしこの方法は格好悪いし非効率だ\footnote{これは新しいCommon Lispの 数列マクロを使ってもっとエレガントに書くこともできる. しかしこれらのマクロはLispそのものの拡張となっているので,結局は同じことだ.}. 代わりに新しい対応関数map1-n(54ページ参照)を定義し, それを次のように呼び出せばいい:

(map1-n fn 10)

関数を定義するのは比較的真っ当な方法だ. マクロは新しいオペレータのさらに一般的な (しかし余り理解されていない)定義方法を提供する. マクロはプログラムを書くプログラムだ. この文には非常に深い意味が込められている. そしてその探求はこの本の主目的の一つなのだ.

マクロを注意して使えば驚異的に明確でエレガントなプログラムができる. これらの宝石のようなプログラムは,何もせずに得られたわけではない. 最後にはマクロは世界で一番自然なものに思えるだろうが,最初は理解が難しい. これはマクロが関数よりも一般的で,書くときに注意すべきことが多いせいもある. しかしマクロの理解が難しい大きな理由は,それが全く異質な(foreign)ものだからだ. Lispのマクロのようなものを持つ言語は他にない. だからマクロについて学ぶと, 他のプログラミング言語からうっかり引き継いだ先入観を振り捨てることに つながるかもしれない. その中の主要なものは,死後硬直に悩まされるものとしてのプログラムという概念だ. なぜデータ構造が流動的で変更可能であるべきなのに,プログラムはそうでないのだろうか? Lispではプログラムがデータであるのだ. しかしこの事実の持つ意味をモノにするには暫く時間がかかる.

マクロに慣れるのに暫く時間がかかっても,それは努力に見合うことだ. 繰り返しのようなありふれた用途でも, マクロはプログラムを目覚ましく小型できれいなものに変える. ここで,あるプログラムがコード本体をxについてaからbまで 繰り返さなければならないとしよう. Lisp組み込みのdoはもっと一般的な目的のためのものだ. それを単純な繰り返しに使うと一番読み易いコードにはならない:

(do ((x a (+ 1 x)))
    ((> x b))
  (print x))

代わりにこうするだけでいい:

(for (x a b)
  (print x))

マクロがこれを可能にする. 6行のコードで(154ページ参照)for文をこのプログラミング言語に追加できる. そしてそれは初めからあった構文のように機能する. そして後の章で見るように,forの実装はマクロでできることの手始めでしかない.

Lispの拡張で一度に使えるのは関数やマクロ1個ずつに限られるわけではない. 必要とあればあるプログラミング言語全体をLispの上に構築し, それを使ってプログラミングすることもできる. Lispはコンパイラやインタプリタを書くのに最適のプログラミング言語だが, 新しいプログラミング言語を定義する別の方法を提供する. その方がしばしばエレガントだし,労力が少なく済むのは確かだ: それはLispを修正して新しいプログラミング言語を定義する方法だ. そうすればLispの機能のうち新しいプログラミング言語でも変更せずに使える部分 (例えば算術演算やI/O)はそのまま利用でき, 異なっている部分(例えば制御構造)だけ実装すればいい. このように実装されたプログラミング言語を埋め込み言語と呼ぶ.

埋め込み言語はボトムアップ・プログラミングの自然な帰結だ. Common Lispには既に幾つか例がある. 一番有名なCLOSについては後の章で議論する. しかし自分だけの埋め込み言語を定義することもできる. 自分のプログラムに適した埋め込み言語を作ることができるが, それがLispとかけ離れたようなものになってもいい.

なぜ(またはいつ)Lispか

これらの新たな可能性は魔法の要素たった1個から生まれる訳ではない. この点を見れば,Lispはアーチのようなものだ. 楔形の石(voussoirs)のうち,どれがアーチを支えているのか? これは質問そのものが誤っている:どの石もアーチを支えているのだ. アーチと同様,Lispは組み合わさった機能の集合体だ. それらの幾つかをここで挙げることができる ---動的メモリ割り当てとガーベジ・コレクション,実行時型指定, オブジェクトとしての関数,リストを生成する組み込みパーサ, リストとして表現されたプログラムを受け付けるコンパイラ, インタラクティブな環境等々--- しかしどの一つをとってもLispの持つ力の理由にはならない. LispプログラミングをLispプログラミングたらしめているのは,そのコンビネーションだ.

過去20年でプログラミングの方法は変化した. インタラクティブな環境,動的リンク,オブジェクト指向プログラミングまで ---これらの変化の多くは,Lispの柔軟性を幾分かでも他のプログラミング言語に与えようとする 細切れの試みだった. アーチの喩えは,それらがどれ程成功を収めたかを示唆している.

LispとFortranが現存するプログラミング言語のうち最も古いものだということは, よく知られている. おそらくもっと意味のある事実は,それらはプログラミング言語のデザインの思想のうち 対極にあるものを代表しているということだ. Fortranはアセンブリ言語からの進歩として開発された. Lispはアルゴリズムを表現するプログラミング言語として開発された. そういった異なる意図は,大きく異なるプログラミング言語を生んだ. Fortranはコンパイラ開発者の人生を楽にしてくれる. Lispはプログラマの人生を楽にしてくれる. それ以来,大抵のプログラミング言語は二つの極の間のどこかに位置するものだった. そしてFortranとLispは真ん中に向かって歩み寄ってきた. Fortranは今ではずいぶんAlgolに似てきたし, Lispは若かりし頃の無駄な習慣を幾つか諦めた.

最初のFortranとLispは一種の戦場のようなものを定義した. 片方では鬨の声は「効率〜!(実装は辛すぎる)」で, もう片方では鬨の声は「抽象化〜!(商用ソフトではあり得ない)」だ. 古代ギリシャの戦争の結果を神々が高みから決めたように, この戦の結果はハードウェアによって決められるようになっている. 年を追うごとに情勢はLisp側に有利になってきているようだ. 今ではLispに対する議論は,1970年代初頭にアセンブリ言語プログラマ達が 高水準プログラミング言語に対して仕掛けた議論に非常に似てきた. 今や問いはなぜLispか?ではなくいつLispか?になっている.


←: 前書き     ↑: On Lisp     →: 関数

Copyright (c) 2003-2011 野田 開     NODA Kai <nodakai@gmail.com>