ウェブベースアプリケーションのためのLisp

原文:Lisp for Web-Based Applications


普通のやつらの上を行けへのリンクがslashdotにアップされた後に、何人かの読者は、私たちがViawebでLispを使ったことで得られた固有の技術的なアドバンテージについて、さらに詳しく聞きたがった。 興味を持つ人のために、私が2001年4月にマサチューセッツ州ケンブリッジのBBN研究所で行った講演の要約を以下に示す。


ウェブベースアプリケーションのためのLisp

ポール グレアム

(このエッセイは2001年4月にマサチューセッツ州ケンブリッジのBBN研究所で行った講演の要約である)

お望みのどんな言語でも

ウェブベースのアプリケーションを書く際にLispを使う理由の一つは、Lispを使えるってことだ。 自分のサーバだけで動くソフトウェアを書いているなら、どんな言語を使うことだってできる。

長い間、アプリケーションを書くのにどんな言語を使用したらよいか、その選択の余地がプログラマにはたいしてなかった。 最近まで、アプリケーションプログラムを書くということはデスクトップコンピュータで動くソフトウェアを書くことだった。 デスクトップソフトウェアでは、オペレーティングシステムと同じ言語でアプリケーションを書くように仕向けられる強い傾向があった。 10年前は、あらゆる実用目的に対して、アプリケーションはC言語で書かれていた。

ウェブベースのアプリケーションでは、それが変わる。 あなたはサーバを支配することができ、あなたが望む言語でソフトウェアを書いていい。 現在ではオペレーティングシステムとコンパイラの両方のソースコードがあるのを当然と考えることができる。 言語とオペレーティングシステムの間に何か問題があったら、自分でそれを修正できる。

だが、この新しい自由は諸刃の剣だ。 多くの選択肢があるってことは、どの選択をしたらよいかを考える必要がある、ということだからだ。 昔はもっと単純だった。 あなたがソフトウェアのプロジェクトを担当していて、ある問題児が「今までのとは違う言語でソフトを書こう」って言い出したら、単に「実用的じゃない」って言って、それでおしまい。

現在のサーバベースのアプリケーションでは、すべてが変わった。 どんな言語を選ぶかが、市場の力に影響されることがある。 大多数の競争相手がしているように、単にCとC++を使い、何も変化していないふりをしてしたら、あなたは自分自身が落ちぶれるための準備をしている。 より強力な言語を使用する小さいベンチャーが、あなたのおまんまを食べてしまうことだろう。

インクリメンタルな開発

Lispでのソフトウェア開発には、あるスタイルがある。 そのような伝統の一つがインクリメンタル(漸進的)な開発だ。 ほとんど何もしないようなプログラムをできるだけ速く書くことから始める。 その後、少しずつ特徴を加えていくが、どの段階でも動くコードがある。

私はこの方法は、よりよいソフトウェアをすばやく手に入れることができると思う。 Lispのあらゆることがこのプログラミングスタイル向けにチューンされている。 なぜってLispプログラマはこの方法で、少なくとも30年以上はうまくいっているからだ。

Viawebエディタはインクリメンタルな開発の最も極端なケースの一つにちがいない。 それはViaweb社を始める直前に私が書いた本の中でサンプルとして使用した、ウェブサイトを生成する120行のプログラムから始まった(訳注4.)。 Viawebエディタはこのプログラムからインクリメンタルに成長していって、最終的におよそ2万5000行のコードになった。 私はけっして仕切りなおして、全体を書き直したりはしなかった。 1日や2日でも動くコードがなかったら、今のように大きくなっていたとは思わない。 開発過程全体は、ひとつの長いゆるやかな変化の連続だった。

この開発スタイルは、ウェブベースのソフトウェアで可能なローリングリリースにうまくフィットする。 またそれは一般的に、ソフトウェアをより速く書く方法の一つでもある。

対話的トップレベル

Lispの対話的トップレベルは、ソフトウェアをすばやく開発するためにとても役立つ。 だが最大のアドバンテージは、たぶんバグを見つけることに関してだ。

前に述べたように、ウェブベースのアプリケーションでは、ユーザのデータはあなたのサーバにあるので、たいていの場合バグを再現できるのである。 カスタマサポートがViawebエディタのバグレポートを持ってくると、私はコードをLispインタプリタにロードし(訳注1.)、そのユーザのアカウントにログインする。 もし、そのバグを再現できたなら、私は現実のブレークループ(訳注2.)に到達しており、何が失敗しようとしていたのかを正確に知ることができる。コードを修正して即座にリリースできることも多いだろう。 ここで「即座に」というのは、ユーザがまだカスタマサポートに電話している間にということである。

そのようにすばやくバグフィックスを行うことで、私たちの立場はありえないほど有利になった。まだ電話している間にバグを捕まえて修正できたなら、カスタマサポートに勘違いと考えられているという印象をユーザに与えることができたのだ。 ときには、(カスタマサポートにとっては喜ばしいことに)「もう一度ログインしてみて、まだ問題が起こるかどうか調べてください」とユーザに伝えてもらえた。 もちろん、ユーザがログインしなおすと、バグが修正された新しいバージョンのソフトウェアがあり、全てはうまく動作するのだ。 これは少しズルいとは思うが、すごく面白かった。

HTMLのためのマクロ

Lispのマクロは私たちにとって別の大きな幸運だった。 Viawebエディタの中では、非常に広範囲にわたってマクロを使った。 正確には、Viawebエディタが一つの大きなマクロと言えるかもしれない。 このことで、Lispがどれほと頼りになったか分かるだろう。 Lispと同じようなマクロを持っている言語は他にないのだから。

マクロの用途の一つは、HTMLを生成することだった。 マクロとHTMLにはとても自然な親和性がある。 HTMLはLispのように前置記法だし、Lispのように再帰的だからだ。 私たちは、最も複雑で込み入ったHTMLを生成する際に、マクロを定義するマクロを使っているが、それでいてそのマクロは、とても扱いやすかった。

埋め込み言語

マクロのもう一つの大きな用途は、Rtmlと呼ばれるページ記述用の埋込み言語だった。 (私たちはRtmlが何を表すかについて様々な説明をしたが、本当は私がViawebのもう一人の創業者であるロバートモリスにちなんで名付けた。彼のログイン名がRtmなのだ。)

私たちのソフトウェアによって作られたあらゆるページがRtmlで書かれたプログラムによって生成された。 誰かを脅かすことのないように、これらのプログラムをテンプレートと呼んでいたが、これらは正真正銘のプログラムだ。 実際、これらはLispのプログラムだった。 RtmlはマクロとLispの組み込みオペレータでできていた。

ユーザは、自分のページをどう見せたいかを記述するために、独自のRtmlテンプレートを書くことができた。 私たちには、Interlispの構造エディタによく似たテンプレートを操作するためのエディタがあって、ユーザは自由形式のテキストをタイプする代わりに、コードの断片をカットしたりペーストしたりする。 これは構文エラーを起こさなくなることを意図したものだ。 また、S式の背後にあるカッコを表示する必要がなくなることも意図していた。 構造をインデントで見せることができたからだ。 このようにして、私たちは言語があまりユーザを脅かさないようにしていた。

また、私たちはRtmlが実行時にエラーを起こさないように設計した: あらゆるRtmlプログラムは何らかのウェブページを出力するのだ。 そうすることで、所望のページが出力されるまで、ユーザはRtmlプログラムを修正してデバッグできる。

私たちは当初、ユーザがウェブコンサルタントになると予想し、彼らがRtmlを大いに使うことを期待していた。 私たちはセクションページやアイテムページなどにデフォルトのテンプレートをいくつか用意して、ユーザがそれらを手に取り修正して、所望のページを作成できるように考えていた。

実際には、ウェブコンサルタントはViawebを好まないことが判った。 一般にコンサルタントとは、クライアントが使うには難しすぎる製品を使うことを好む。 それが彼らの継続的雇用を保障するからだ。 コンサルタントが私たちのウェブサイトに来ると、このソフトウェアはとても使うのが簡単で、誰でも5分でオンラインストアを作れると言い、それからこのソフトウェアを使っていくことはできないと言うだろう。 それで、私たちはウェブコンサルタントからあまり関心を得ることがなかった。 代わりとなるユーザは皆エンドユーザ、すなわち実際の商売人である傾向があった。 彼らは自分のウェブサイトをコントロールできるという考え方を好んだ。 そして、この手のユーザはプログラミングをしたいとはまったく思っていなかった。 彼らはただデフォルトの内蔵テンプレートを使用した。

結局、Rtmlはプログラムのメインのインタフェースにはならなかった。 Rtmlは二つの役割を果たすことになった。 まず第一に、それは本当に洗練されたユーザのための抜け道になった。 彼らは内蔵テンプレートが提供できなかった何かを望むのだ。 Viawebをやっている間のどこかで、誰かが私に非常に有益な忠告をくれた: ユーザはたとえ採用することがなくても常にアップグレードパスを欲しがるものだと。 Rtmlは私たちが提供するアップグレードパスになった。 もしユーザが望むのならば、彼らは自分のページのあらゆることを絶対的にコントロールできる。

数百人のユーザのうちのたった一人が、実際に自分自身でテンプレートを書いた。 そして、それがRtmlの2番目のアドバンテージにつながった。 こうしたユーザが内蔵テンプレートを変更する方法を観察することで、我々が何を追加しなければならないかが分かったのだ。 私たちは最終的に、誰もRtmlを使わないですむようにすることを目標にした。 私たちの内蔵テンプレートは、人々が望む全てのことをするべきなのだ。 この新しいアプローチの中では、Rtmlは私たちのソフトウェアに何か欠けているという警告を出してくれた。

Rtmlを使うことで得た3番目の、そして一番の幸運は、私たち自身がRtmlから得たアドバンテージだった。 たとえ私たちがRtmlの唯一のユーザだったとしても、そのようにソフトウェアを書くことには、とても価値があったのだ。 自分たちのソフトウェアの中に追加の抽象レイヤを持つことは、競争相手に対する大きなアドバンテージをもたらした。 一つには、私たちのソフトウェアは、かなりすっきりした設計になった。 競争相手のように、単にウェブページを生成するC言語やPerlの実際のちょっとしたコードを持つのででなく、私たちはウェブページを生成するための非常に高級な言語と、その言語で記述されたページスタイル集を持つことになった。 そして、実際のコードもずっとすっきりとして、変更も容易になった。 ウェブベースのアプリケーションは多くの小さな変更の連続としてリリースされることは既に述べたが、そのようなリリースを行う場合に、ある変更がいかに重大なのかを知りたくなる。 コードを複数のレイヤに分割することで、この問題をうまく扱うことができる。 下位のレイヤー(Rtml自身)の変更は、滅多に行われない重大な問題になる。 一方で、最上位のレイヤ(テンプレートコード)の変更は、結果をあまり心配することなく、すばやく行うことができるのだ。

RtmlはとてもLisp的な仕事だった。 まず第一に、それは大部分がLispのマクロだった。 オンラインエディタは舞台裏でS式を操作した。 そして、ユーザがテンプレートを操作すると、compile関数が呼び出されてテンプレートをLispの関数にコンパイルした。

Rtmlは、キーワードパラメータにも大きく依存していた。それまでは私が常々 Common Lisp のうさんくさい特徴と考えていた機能にである。 ウェブベースのソフトウェアは、それがリリースされる方法のせいで、変更が容易なように設計しなければならない。 それで、Rtml自身も、ソフトウェアの他の部分と同様に、変更が容易でなければならなかった。 Rtmlのオペレータの大部分はキーワードパラメータをとるように設計されており、どれほどそれで助けられたと判っただろう。 もし、Rtmlオペレータの一つの振舞いに別の次元を加えたくなったら、単に新しいキーワードパラメータを追加すればよく、みんなの既存のテンプレートは引き続き動作するだろう。 Rtmlのいくつかのオペレータがキーワードパラメータをとらないのは、今後変更が必要になると全く思わなかったからなのだが、その殆ど全てに対して、後に自分に腹を立てることになった。 もし、元に戻って最初からやり直すことができるのならば、変更することの一つは、全てのRtmlオペレータがキーワードパラメータをとることにするだろう。

Viawebエディタの中には、実際には二つの埋め込み言語があった。 もう一つの言語とは、利用者には直接見せられてはいなかったが、画像を記述するためのものである。 ViawebはC言語で記述された画像ジェネレータを備えていて、このジェネレータは画像の記述を受け取って画像データの生成を行い、そのデータへのURLを返すことができた。 このような画像の記述も、S式を用いて行った。

サブルーチンをシミュレートするクロージャ

ウェブページをUIとして使用することの問題の一つは、ウェブセッションが本来ステートレスということだ。 私たちは、レキシカルクロージャを使ってサブルーチンのような振舞いをシミュレートすることで、この問題を回避した。 あなたが継続(continuation)について理解しているなら、私たちがやったことを説明する一つの方法は、私たちは継続渡し形式(continuation-passing style)でソフトウェアを書いたということだ。

ほとんどのウェブベースのソフトウェアでは、リンクを生成するとき「ユーザがこのリンクをクリックしたら、このcgiをこの引数で呼び出してほしい」と考えることが多い。 私たちのソフトウェアでは、リンクを生成するとき「ユーザがこのリンクをクリックしたら、この一片のコードを実行してほしい」と考えることができる。ここでいう一片のコードは、値が周辺の環境から定まる自由変数を含むかもしれない任意のコードだ(そして実際はたいていそのような自由変数を含む)。

これを実現する方法として、クロージャであることを想定する第1の引数をとり、コードの本体が続くマクロを書いた(訳注3.)。 そのコードはグローバルなハッシュ表にユニークなIDをキーとして格納され、コードが生成するあらゆる出力は、そのハッシュキーをURLに含んだリンクの中身に現れる。 そのリンクが次にクリックされたなら、私たちのソフトウェアはハッシュ表から対応するコードの断片を探し出し、呼び出して実行する。そして、実行により生成されたページが継続していく。 事実上、私たちはcgiスクリプトを実行時に生成している(このスクリプトが周辺の環境を参照するかもしれないクロージャであることを除けば)。

ここまでだと理屈にすぎないように思われるので、このテクニックが明らかな違いを生み出す例を示そう。 ウェブベースのアプリケーションの中でたびたび行いたいことの一つに、様々な種類のプロパティを持ったオブジェクトの編集をすることがある。 オブジェクトのいくつものプロパティはフォームのフィールドやメニューで表すことができる。 例えば人を表すオブジェクトを編集するときは、まずその人の名前に対応するフィールドを取得し、メニューから役職を選択して、という具合になる。

ここで、あるオブジェクトに色のプロパティがあると、どうなるだろうか? もし、ページの下側にある更新ボタン使って、すべてのことを一つのフォームで行わないといけない普通のcgiスクリプトを使うとしたら、苦労することだろう。 テキストフィールドを使ってユーザーにRGBの数値をタイプさせることもできるが、エンドユーザはそれが好きではない。 可能な色のメニューを持つこともできるが、そうすると可能な色を制限しなければならない。 さもなければ、標準のウェブのカラーマップを提供するためだけにさえ、256個のほとんど区別できない名前のメニュー項目が必要になる。

私たちはViawebで、現在の値を表す色の見本と「変更」と書かれたボタンを表示した。 ユーザが変更ボタンをクリックすると、選択可能な色のイメージマップのページに移動する。 色を選ぶと、オブジェクトの色が変わってプロパティの編集画面に戻ってくる。 これが私がサブルーチンのような振舞いをシミュレートする、という意味だ。 私たちのソフトウェアは、まるで色を選んだところから戻るかのように振舞うことができた。 もちろんそうではない; それはスタックを戻るように見える、新しいcgi呼び出しである。 しかしクロージャを使うことで、ユーザに、また私たちにも、単にサブルーチンコールをしているように見せることが出来た。 私たちは、「ユーザがこのリンクをクリックしたら、色選択ページに行き、それからここに戻って来い」というコードを書くことができた。 これは、サブルーチンをシミュレートできるという可能性を活かした複数の個所の一つにすぎない。 それにより、私たちのソフトウェアは競争相手のものよりも明らかに洗練されるようになった。


訳注:

  1. Viawebシステムは、Common Lisp処理系としてCLISPを利用していたそうだ。
    ユーザ向けにはRtmlはランタイムエラーを起こさないように設計されていたことと、CLISPではコンパイルコード実行時よりはインタプリタ実行時の方がデバッグし易いことから、ここでインタプリタにコードをロードし直しているのだろう。
  2. ブレークループとは、error, cerror, break等の関数が呼ばれた後に到達するCommon Lisp処理系が提供するデバッガのコマンドループ(に入っている状態)を指す。
  3. この文では、continuation-passing style(CPS)そのものについては あまり説明していないが、一例として CPS形式のマクロfooの定義を以下のように書くことができる。

    (defmacro foo ((k . rest-args) &body body) <マクロ展開形の定義> )

    ここで、第1引数 kがcontinuationに、<マクロ展開形の定義>がコードの本体に対応する。コード本体は、k, rest-args, body等のマクロの引数を使って記述される。CPSでは、本体からリターンする代わりに、返り値に相当する値 v を用いて、continuation k を(funcall k v)と呼び出すことで処理を継続させる。
    fooの呼び出し側は、第1引数を (lambda (v) ... ) のようにクロージャとして与えることが想定されており、... の部分に lambda式を取り囲む 呼び出し側環境で参照可能な変数を利用して、呼び出されたfooから戻った後(としてシミュレートされる部分)の処理を記述できる。
  4. Viaweb社を始める直前に書かれた本とはANSI Common Lispのことで、ウェブサイトを生成する120行のプログラムとは、そのcompanion codeの "; *** web ***" という行から関数gen-move-buttonsの定義までと思われる(空行を除くとピッタリ120行になる)。
More ...