オタクの復讐
Original2002年5月
「私たちは C++ プログラマーを狙っていました。私たちは彼らの多くを Lisp に半分ほど引きずり込むことに成功しました。」
- Java仕様の共同著者であるGuy Steele氏
ソフトウェア業界では、尖った頭の学者と、同じく強力な勢力である尖った髪の上司との間で、絶え間ない争いが繰り広げられています。尖った髪の上司が誰なのかは、誰もが知っていますよね? テクノロジー業界のほとんどの人は、この漫画のキャラクターを知っているだけでなく、そのモデルとなった社内の実際の人物も知っていると思います。
尖った髪のボスは、それ自体は一般的だが、一緒に見られることはめったにない 2 つの特性を奇跡的に兼ね備えています。(a) 彼はテクノロジーについて全く知らない、(b) 彼はテクノロジーについて非常に強い意見を持っている。
たとえば、ソフトウェアを書く必要があるとします。尖った髪の上司は、このソフトウェアがどのように動作するのか全く知らず、プログラミング言語の違いもわかりませんが、どの言語で書くべきかは知っています。その通りです。上司は、Java で書くべきだと考えています。
なぜ彼はそう考えるのでしょうか。尖った髪のボスの頭の中を覗いてみましょう。彼は次のようなことを考えています。Java は標準です。私はいつも報道でそれについて読んでいるので、そうに違いないとわかっています。標準なので、それを使用しても問題にはなりません。また、それは常に多くの Java プログラマーが存在することを意味します。したがって、私のために働いているプログラマーが (不思議なことにいつもそうであるように) 今私のために働いているプログラマーが辞めても、私は簡単に彼らを補充できます。
まあ、これはそれほど不合理な話ではないようです。しかし、それはすべて 1 つの暗黙の仮定に基づいており、その仮定は誤りであることが判明しました。尖った髪のボスは、すべてのプログラミング言語はほぼ同等であると信じています。それが本当であれば、彼は正しいでしょう。言語がすべて同等であれば、もちろん、他の人が使用している言語を使用すればいいのです。
しかし、すべての言語が同等というわけではありません。言語間の違いに立ち入ることなく、このことを証明できると思います。1992 年に尖った髪の上司にソフトウェアはどの言語で書くべきか尋ねたとしたら、彼は今日と同じようにためらうことなく答えたでしょう。ソフトウェアは C++ で書くべきです。しかし、言語がすべて同等であるなら、尖った髪の上司の意見が変わるはずがありません。実際、Java の開発者が新しい言語を作ろうとする理由などあるでしょうか。
おそらく、新しい言語を作成するのは、それが既存の言語よりも何らかの点で優れていると考えているからでしょう。実際、ゴスリングは最初の Java ホワイト ペーパーで、Java は C++ のいくつかの問題を修正するために設計されたと明言しています。つまり、言語はすべて同等というわけではありません。尖った髪のボスの頭の中をたどって Java に行き、さらに Java の歴史をたどってその起源までさかのぼると、最初の仮定と矛盾するアイデアにたどり着くことになります。
では、どちらが正しいのでしょうか? James Gosling でしょうか、それとも髪の尖ったボスでしょうか? 当然ながら、Gosling は正しいです。特定の問題に対しては、一部の言語が他の言語よりも優れています。そして、ご存じのように、これは興味深い疑問を提起します。Java は、特定の問題に対して C++ よりも優れているように設計されています。どのような問題でしょうか? Java が優れているのはいつで、C++ が優れているのはいつでしょうか? 他の言語がどちらかよりも優れている状況はあるでしょうか?
この問題を考え始めると、本当に厄介な問題が起こります。尖った髪の上司が、問題の複雑さを完全に考えなければならないとしたら、頭が爆発してしまいます。すべての言語を同等と見なしている限り、最も勢いがあると思われる言語を選択するだけで済みます。これは技術の問題というより流行の問題なので、おそらく彼でも正しい答えを得られるでしょう。しかし、言語が多様であれば、突然、2 つの同時方程式を解かなければならなくなり、自分がまったく知らない 2 つの事柄、つまり、解決する必要のある問題に対する 20 ほどの主要言語の相対的な適合性と、それぞれのプログラマーやライブラリなどを見つける可能性との間の最適なバランスを見つけなければなりません。ドアの向こう側にそれがあるのなら、尖った髪の上司がドアを開けたくないのも不思議ではありません。
すべてのプログラミング言語が同等であると信じることのデメリットは、それが真実ではないということです。しかし、メリットは、生活がずっとシンプルになることです。そして、それがこの考えがこれほど広く普及している主な理由だと思います。これは心地よい考えです。
Java はクールで新しいプログラミング言語なので、かなり優れているに違いないということはわかっています。でも、本当にそうでしょうか? プログラミング言語の世界を遠くから見ると、Java が最新のもののように見えます (十分に離れたところから見ると、Sun が資金を提供している大きな点滅する看板しか見えません)。しかし、この世界を間近で見ると、クールさには程度があることがわかります。ハッカーのサブカルチャーの中には、Java よりもずっとクールだと考えられている Perl という別の言語があります。たとえば、Slashdot は Perl で生成されています。これらの人たちが Java Server Pages を使用しているとは思えません。しかし、Python という別の新しい言語があり、そのユーザーは Perl を見下す傾向があり、さらに待機している言語があります。
これらの言語を Java、Perl、Python の順に見ていくと、興味深いパターンに気付くでしょう。少なくとも、Lisp ハッカーであれば、このパターンに気付くでしょう。各言語は徐々に Lisp に似てきます。Python は、多くの Lisp ハッカーが間違いだと考える機能さえもコピーします。単純な Lisp プログラムを 1 行ずつ Python に翻訳できます。2002 年ですが、プログラミング言語は 1958 年にほぼ追いついています。
数学に追いつく
私が言いたいのは、Lisp は 1958 年に John McCarthy によって初めて発見され、人気のあるプログラミング言語は、彼が当時開発したアイデアにようやく追いつきつつあるということです。
さて、どうしてそれが本当なのでしょうか? コンピュータ技術は急速に変化するものではありませんか? つまり、1958 年当時、コンピュータは冷蔵庫ほどの大きさで、処理能力は腕時計程度でした。そんなに古い技術が、最新の技術より優れているどころか、関連性があるはずがありません。
その理由をお話ししましょう。Lisp はプログラミング言語として設計されたわけではないからです。少なくとも、今日私たちが意味する意味ではそうではありません。プログラミング言語とは、コンピュータに何をすべきかを指示するために使用するものです。McCarthy は最終的にこの意味でのプログラミング言語を開発するつもりでしたが、実際に完成した Lisp は、彼が理論的な演習として行った別のもの、つまりチューリング マシンのより便利な代替手段を定義するための取り組みに基づいていました。McCarthy は後に次のように述べています。
Lisp がチューリング マシンよりも優れていることを示す別の方法は、汎用 Lisp 関数を記述し、それが汎用チューリング マシンの記述よりも簡潔でわかりやすいことを示すことでした。これは Lisp 関数eval ... で、Lisp 式の値を計算します...。 evalを記述するには、Lisp 関数を Lisp データとして表す表記法を考案する必要がありましたが、そのような表記法は論文の目的で考案されたもので、実際に Lisp プログラムを表現するために使用されることは考慮されていませんでした。
その後、1958 年後半のある時期に、マッカーシーの大学院生の 1 人であるスティーブ ラッセルがevalの定義を見て、これを機械語に翻訳すると Lisp インタープリタになることに気づきました。
これは当時大きな驚きでした。マッカーシーは後にインタビューでこれについてこう語っています。
スティーブ・ラッセルは、このeval をプログラムしてみないか、と言いました。私は、理論と実践を混同している、このeval は読むためのもので、計算するためのものではない、と言いました。しかし、彼はそれを実行しました。つまり、私の論文のevalを [IBM] 704 マシン コードにコンパイルしてバグを修正し、これを Lisp インタープリタとして宣伝したのです。確かに、それは Lisp インタープリタでした。つまり、その時点で Lisp は基本的に今日の形になっていました...
突然、数週間のうちに、マッカーシーは彼の理論的な演習が実際のプログラミング言語に変わったことに気づいた。そして、それは彼が意図していたよりも強力なものだった。
したがって、この 1950 年代の言語が時代遅れではない理由を簡単に説明すると、それはテクノロジーではなく数学であり、数学は古びないということです。Lisp と比較するのに適したものは 1950 年代のハードウェアではなく、たとえば、1960 年に発見され、現在でも最速の汎用ソートであるクイックソート アルゴリズムです。
1950 年代から現在まで生き残っている言語がもう 1 つあります。Fortran は、言語設計に対する正反対のアプローチを表しています。Lisp は、予期せずプログラミング言語に変わった理論でした。Fortran は、意図的にプログラミング言語として開発されましたが、現在では非常に低レベルの言語と考えられています。
1956 年に開発された言語であるFortran Iは、現在の Fortran とはまったく異なるものでした。Fortran I は、数学的処理を備えたアセンブリ言語とほとんど同じでした。いくつかの点で、より最近のアセンブリ言語よりも機能が劣っていました。たとえば、サブルーチンはなく、分岐しかありませんでした。現在の Fortran は、Fortran I よりも Lisp に近いと言えるでしょう。
Lisp と Fortran は、数学に根ざし、マシン アーキテクチャに根ざした 2 つの別々の進化樹の幹でした。この 2 つの樹は、それ以来ずっと収束してきました。Lisp は最初は強力でしたが、その後 20 年間で高速になりました。いわゆる主流言語は最初は高速でしたが、その後 40 年間で徐々に強力になり、現在では最も先進的な言語は Lisp にかなり近いものになっています。近いですが、まだいくつか欠けているものがあります...
Lisp が他と違う点
Lisp が最初に開発されたとき、9 つの新しいアイデアが具体化されました。これらのアイデアのいくつかは今では当たり前になっていますが、他のものはより高度な言語でしか見られません。2 つは Lisp 独自のものです。9 つのアイデアは、主流に採用された順に並べると、
条件文。条件文は if-then-else 構造です。今では当たり前のことですが、Fortran I にはこれがありませんでした。基礎となるマシン命令に厳密に基づく条件付き goto しかありませんでした。
関数型。Lisp では、関数は整数や文字列のようなデータ型です。関数にはリテラル表現があり、変数に格納したり、引数として渡したりすることができます。
再帰。Lisp はこれをサポートした最初のプログラミング言語でした。
動的型付け。Lisp では、すべての変数は実質的にポインタです。型を持つのは変数ではなく値であり、変数を割り当てたりバインドしたりするということは、ポインタが指すものではなく、ポインタをコピーすることを意味します。
ガベージコレクション。
プログラムは式で構成されています。Lisp プログラムは式のツリーであり、それぞれが値を返します。これは、式と文を区別する Fortran やその後継言語のほとんどとは対照的です。
Fortran I ではステートメントをネストすることができなかったため、この区別が存在するのは当然でした。そのため、数学が機能するためには式が必要でしたが、値を待つものが何もなかったため、他の何かに値を返すようにしても意味がありませんでした。
この制限はブロック構造言語の登場とともに解消されましたが、その時はもう遅すぎました。式と文の区別は定着し、Fortran から Algol に、そしてその後その両方の子孫に広がりました。
シンボル型。シンボルは、ハッシュ テーブルに格納されている文字列へのポインターです。したがって、各文字を比較するのではなく、ポインターを比較することで等価性をテストできます。
シンボルと定数のツリーを使用したコードの表記法。
言語全体が常に存在します。読み取り時、コンパイル時、実行時に実際の区別はありません。読み取り中にコードをコンパイルまたは実行したり、コンパイル中にコードを読み取ったり実行したり、実行時にコードを読み取ったりコンパイルしたりできます。
読み取り時にコードを実行すると、ユーザーは Lisp の構文を再プログラムできます。コンパイル時にコードを実行することはマクロの基礎です。実行時にコンパイルすることは、Emacs などのプログラムで Lisp を拡張言語として使用する基礎です。実行時にコードを読み取ると、プログラムは S 式を使用して通信できます。このアイデアは最近 XML として再発明されました。
Lisp が初めて登場したとき、これらのアイデアは、1950 年代後半に利用可能だったハードウェアによって大きく左右される通常のプログラミング手法とはかけ離れていました。時が経つにつれ、一連の人気言語に具体化されたデフォルト言語は、徐々に Lisp へと進化してきました。アイデア 1 から 5 は現在広く普及しています。6 は主流になり始めています。Python には 7 の形式がありますが、構文はないようです。
8 番目については、これが最も興味深いかもしれません。アイデア 8 と 9 は、Steve Russell が McCarthy が決して実装するつもりのなかったものを実装したため、偶然に Lisp の一部になりました。しかし、これらのアイデアは Lisp の奇妙な外観と最も特徴的な機能の両方の原因であることがわかりました。Lisp が奇妙に見えるのは、奇妙な構文があるからではなく、構文がないからです。他の言語が解析されるときに舞台裏で構築される解析ツリーでプログラムを直接表現します。これらのツリーは、Lisp のデータ構造であるリストで構成されています。
言語を独自のデータ構造で表現することは、非常に強力な機能であることがわかります。アイデア 8 と 9 を組み合わせると、プログラムを作成するプログラムを作成できることになります。奇妙なアイデアのように聞こえるかもしれませんが、Lisp では日常的に行われています。これを実行する最も一般的な方法は*、マクロと呼ばれるものを使用することです。*
Lisp における「マクロ」という用語は、他の言語での意味とは異なります。Lisp マクロは、略語から新しい言語のコンパイラまで、あらゆるものになり得ます。Lisp を本当に理解したい場合、またはプログラミングの視野を広げたい場合、マクロについてさらに学ぶことをお勧めします。
私の知る限り、マクロ (Lisp の意味での) は依然として Lisp に特有のものです。これは、マクロを使用するには、言語を Lisp と同じくらい奇妙に見せなければならないことが一因です。また、最終的なパワーの増分を追加した場合、もはや新しい言語を発明したと主張できず、Lisp の新しい方言を発明したとしか主張できないためかもしれません。
これはほとんど冗談として言っているのですが、まったく真実です。car、cdr、cons、quote、cond、atom、eq、およびリストとして表現される関数の表記法を持つ言語を定義すれば、Lisp の残りの部分はすべてそこから構築できます。実際、これが Lisp を定義する性質です。McCarthy が Lisp に現在の形を与えたのは、これを実現するためでした。
言語が重要な場所
では、Lisp が主流の言語が漸近的に近づいている一種の限界を表していると仮定すると、ソフトウェアを書くのに実際に Lisp を使うべきなのでしょうか? それほど強力でない言語を使うと、どれだけ失うものがあるのでしょうか? 時には、革新の最先端にいない方が賢明ではないでしょうか? そして、ある程度、人気はそれ自体が正当化されるのではないでしょうか? たとえば、とんがり頭の上司が、プログラマーを簡単に雇える言語を使いたいと思うのは当然ではないでしょうか?
もちろん、プログラミング言語の選択があまり重要でないプロジェクトもあります。原則として、アプリケーションの要求が厳しいほど、強力な言語を使用することで得られる効果は大きくなります。しかし、要求がまったく厳しくないプロジェクトもたくさんあります。プログラミングのほとんどは、おそらく小さなグルー プログラムの作成で構成されており、小さなグルー プログラムには、既に使い慣れていて、必要な作業に適したライブラリがある言語を使用できます。Windows アプリから別のアプリにデータを渡すだけであれば、もちろん Visual Basic を使用してください。
Lisp でも小さなグルー プログラムを書くことができます (私はデスクトップ電卓として使っています)。しかし、Lisp のような言語の最大のメリットは、スペクトルの反対側、つまり熾烈な競争に直面して難しい問題を解決するために洗練されたプログラムを書く必要がある場合にあります。良い例は、ITA Software が Orbitz にライセンス供与している航空運賃検索プログラムです。この 2 社は、すでに 2 つの大手で定着した競合企業である Travelocity と Expedia が独占している市場に参入し、技術的に 2 社を屈辱したようです。
ITA のアプリケーションの中核は、200,000 行の Common Lisp プログラムです。このプログラムは、メインフレーム時代のプログラミング手法をまだ使用していると思われる競合他社よりもはるかに多くの可能性を検索します (ただし、ITA も、ある意味ではメインフレーム時代のプログラミング言語を使用しています)。私は ITA のコードを一度も見たことがありませんが、同社のトップ ハッカーの 1 人によると、同社はマクロを大量に使用しており、それを聞いても驚きません。
求心力
珍しい技術を使うことにコストがかからないと言っているのではありません。尖った髪の上司がこれを心配するのはまったくの間違いではありません。しかし、リスクを理解していないため、リスクを誇張しがちです。
あまり一般的でない言語を使用すると、3 つの問題が発生する可能性があると思います。プログラムが他の言語で書かれたプログラムとうまく連携しない可能性があります。利用できるライブラリが少なくなるかもしれません。また、プログラマーを雇うのに苦労するかもしれません。
これらはそれぞれどの程度問題なのでしょうか? 最初の問題の重要性は、システム全体を制御できるかどうかによって異なります。バグのあるクローズド オペレーティング システム (名前は挙げません) 上でリモート ユーザーのマシンで実行する必要があるソフトウェアを作成する場合、アプリケーションを OS と同じ言語で作成することに利点があるかもしれません。しかし、ITA がおそらくそうであるように、システム全体を制御してすべての部分のソース コードを持っている場合は、任意の言語を使用できます。互換性の問題が発生した場合は、自分で修正できます。
サーバーベースのアプリケーションでは、最先端の技術を使用しても問題ありません。これが、Jonathan Erickson 氏が「プログラミング言語のルネッサンス」と呼ぶものの主な原因だと思います。Perl や Python などの新しい言語が話題になるのも、このためです。これらの言語が話題になるのは、Windows アプリケーションの作成に使用されているからではなく、サーバーで使用されているからです。また、ソフトウェアがデスクトップからサーバーに移行するにつれて (Microsoft でさえもその未来を諦めているようです)、中途半端な技術を使用するプレッシャーはますます少なくなるでしょう。
ライブラリに関しても、その重要性はアプリケーションによって異なります。それほど要求の厳しくない問題の場合、ライブラリの可用性が言語の本来の力を上回ることがあります。損益分岐点はどこでしょうか。正確に言うのは難しいですが、どこであろうと、アプリケーションと呼べるものが不足しています。ソフトウェア ビジネスを営む会社が、自社製品の 1 つとなるアプリケーションを作成する場合、おそらく複数のハッカーが関与し、作成に少なくとも 6 か月かかります。その規模のプロジェクトでは、強力な言語が既存のライブラリの利便性を上回り始めるでしょう。
とんがり髪の上司の 3 番目の心配事、プログラマーを雇うことの難しさは、誤解を招くと思います。結局、何人のハッカーを雇う必要があるのでしょうか? 今では、ソフトウェアは 10 人未満のチームで開発するのが最適であることは誰もが知っています。また、聞いたことのある言語であれば、その規模のハッカーを雇うのに苦労することはないはずです。Lisp ハッカーを 10 人見つけられないのであれば、おそらくあなたの会社はソフトウェア開発に適した都市に拠点を置いていないのでしょう。
実際、より強力な言語を選択すると、必要なチームの規模が縮小する可能性があります。これは、(a) より強力な言語を使用すると、それほど多くのハッカーは必要なくなる可能性があり、(b) より高度な言語で作業するハッカーはより賢い可能性が高いためです。
「標準」技術とみなされているものを使用するよう、多くの圧力がかからないと言っているわけではありません。Viaweb (現在の Yahoo Store) では、Lisp を使用することで、ベンチャー キャピタルや買収候補の間で眉をひそめられました。しかし、Sun のような「産業用」サーバーの代わりに汎用の Intel ボックスをサーバーとして使用したこと、Windows NT のような実際の商用 OS の代わりに当時は無名だったオープンソースの Unix バリアントである FreeBSD を使用したこと、今では誰も覚えていないSETと呼ばれる想定上の電子商取引標準を無視したことなどでも、眉をひそめられました。
役員たちに技術的な決定を任せるわけにはいきません。私たちが Lisp を使っていたことで、買収候補の何人かは不安に思ったでしょうか? 少しはそう感じた人もいましたが、Lisp を使っていなかったら、買収したいと思わせるソフトウェアを書くことはできなかったでしょう。彼らにとって異常に思えたことは、実は因果関係だったのです。
スタートアップを始める場合、ベンチャーキャピタルや潜在的な買収者を喜ばせるために製品を設計しないでください。*ユーザーを喜ばせるために製品を設計してください。*ユーザーを獲得できれば、他のすべてはついてきます。そうでない場合は、テクノロジーの選択がいかに快適でオーソドックスなものであったとしても、誰も気にしません。
平均的であることの代償
それほど強力でない言語を使用すると、どれだけの損失があるでしょうか? 実際、それに関するデータがいくつかあります。
最も便利なパワーの尺度は、おそらくコード サイズです。高級言語のポイントは、より大きな抽象化、つまり、より大きなレンガを提供することです。これにより、特定のサイズの壁を建てるのにそれほど多くのレンガは必要なくなります。したがって、言語が強力になればなるほど、プログラムは短くなります (もちろん、文字数だけでなく、個別の要素数も短くなります)。
より強力な言語を使用すると、どのようにしてより短いプログラムを作成できるのでしょうか。言語が許せば、使用できる 1 つの手法は、ボトムアップ プログラミングと呼ばれるものです。単に基本言語でアプリケーションを作成するのではなく、基本言語の上に、自分のプログラムのようなプログラムを作成するための言語を構築し、その言語でプログラムを作成します。結合されたコードは、基本言語でプログラム全体を作成したときよりもずっと短くなります。実際、ほとんどの圧縮アルゴリズムはこのように動作します。ボトムアップ プログラムは、多くの場合、言語層をまったく変更する必要がないため、変更も簡単です。
コードのサイズは重要です。プログラムを書くのにかかる時間は、主にその長さによって決まるからです。プログラムが別の言語で 3 倍の長さになる場合、書くのにも 3 倍の時間がかかります。そして、ある一定のサイズを超えると、新規雇用は実際には純損失となるため、人員を増やすことでこの問題を回避することはできません。フレッド ブルックスは、有名な著書*「人月の神話」でこの現象について説明しましたが、*私が目にしたものはすべて、彼の言ったことを裏付けるものでした。
では、Lisp で書いた場合、プログラムはどのくらい短くなるのでしょうか。たとえば、Lisp と C を比較した私の聞いた数字のほとんどは、7 ~ 10 倍程度です。しかし、 New Architect誌の最近の ITA に関する記事では、「1 行の Lisp は 20 行の C を置き換えることができる」と述べられており、この記事には ITA の社長の発言が多数引用されていることから、この数字は ITA から得たものだと思います。そうであれば、この数字をある程度信頼できます。ITA のソフトウェアには Lisp だけでなく C と C++ が多数含まれているため、ITA は経験に基づいて話しているのです。
私の推測では、これらの倍数は一定ではありません。より困難な問題に直面したときや、より優秀なプログラマーがいるときには、倍数は増加すると思います。本当に優秀なハッカーは、より優れたツールからより多くの成果を引き出すことができます。
いずれにせよ、曲線上の 1 つのデータ ポイントとして、ITA と競合してソフトウェアを C で記述することを選択した場合、ITA はあなたよりも 20 倍速くソフトウェアを開発できます。新しい機能に 1 年を費やした場合、ITA は 3 週間未満でそれを再現できます。一方、ITA が新しい機能の開発に 3 か月しか費やさなかった場合、ITA もそれを実現するまでに5 年かかります。
そして、ご存知ですか? それは最良のシナリオです。コード サイズの比率について話すとき、あなたは暗黙のうちに、より弱い言語で実際にプログラムを作成できると想定しています。しかし、実際には、プログラマーが実行できることには限界があります。低レベルすぎる言語で難しい問題を解決しようとすると、一度に頭に入れておくべきことが多すぎるという状況に陥ります。
したがって、ITA が Lisp で 3 か月で書けるものを ITA の仮想競合企業が再現するには 5 年かかると私が言うとき、何も問題がなければ 5 年かかるという意味です。実際、ほとんどの企業のやり方では、5 年かかる開発プロジェクトは、おそらく決して完了しないでしょう。
これは極端な例だと認めます。ITA のハッカーは異常に賢いようですし、C はかなり低レベルの言語です。しかし、競争の激しい市場では、2 対 1 または 3 対 1 の差でも、常に遅れをとることは間違いありません。
レシピ
これは、尖った髪のボスが考えたくもない類の可能性です。そして、ほとんどのボスは考えません。なぜなら、結局のところ、尖った髪のボスは、誰もそれが自分のせいだと証明できない限り、自分の会社がやられたとしても気にしないからです。彼にとって個人的に最も安全な計画は、群れの中心に留まることです。
大規模な組織では、このアプローチを説明するのに「業界のベスト プラクティス」というフレーズが使われます。その目的は、とんがり頭のボスを責任から守ることです。ボスが「業界のベスト プラクティス」であるものを選択して会社が負けたとしても、ボスを責めることはできません。ボスが選択したのではなく、業界が選択したのです。
この用語は、もともと会計方法などを説明するために使われていたと思います。大まかに言えば、*奇妙なことをしないということです。*会計では、おそらくそれが良い考えでしょう。「最先端」と「会計」という言葉は、一緒に使うとあまり良い響きではありません。しかし、この基準をテクノロジーに関する決定に持ち込むと、間違った答えが出始めます。
多くの場合、テクノロジーは最先端であるべきです。プログラミング言語では、Erann Gat が指摘したように、「業界のベスト プラクティス」で実際に得られるのは最高のものではなく、単に平均です。決定によって、より積極的な競合他社の数分の 1 の速度でソフトウェアを開発する必要がある場合、「ベスト プラクティス」は誤った名称です。
ここに、私が非常に価値があると思う 2 つの情報があります。実際、私は自分の経験からそれを知っています。1 つ目は、言語の力にはばらつきがあるということです。2 つ目は、ほとんどのマネージャーが意図的にこれを無視していることです。この 2 つの事実は、文字通りお金を稼ぐためのレシピです。ITA は、このレシピが実際に機能している例です。ソフトウェア ビジネスで勝ちたいなら、見つけられる最も難しい問題に取り組み、入手できる最も強力な言語を使用し、競合他社の尖った髪の上司が平凡な態度に戻るのを待つだけです。
付録: 電力
プログラミング言語の相対的な力について私が言いたいことの例として、次の問題を考えてみましょう。累算器を生成する関数、つまり数値 n を受け取り、別の数値 i を受け取り、i だけ増加した n を返す関数を返す関数を記述します。
(これはプラスではなく ずつ増加します。アキュムレータは累積する必要があります。)
Common Lispでは次のようになります
(defun foo (n) (lambda (i) (incf ni)))
そしてPerl 5では、
sub foo { my ($n) = @_; sub {$n += shift} }
Perl ではパラメータを手動で抽出する必要があるため、Lisp バージョンよりも要素が多くなります。
SmalltalkではコードはLispよりも少し長くなります
foo: n |s| s := n. ^[:i| s := s+i. ]
一般的には字句変数は機能しますが、パラメータへの割り当てはできないため、新しい変数 s を作成する必要があります。
Javascript では、文と式の区別が保持されるため、例が少し長くなります。そのため、値を返すには明示的な return 文が必要です。
function foo(n) { return function (i) { return n += i } }
(公平を期すために言うと、Perl もこの区別を保持していますが、リターンを省略できるようにすることで、典型的な Perl のやり方でこれを処理します。)
Lisp/Perl/Smalltalk/Javascript コードを Python に翻訳しようとすると、いくつかの制限に遭遇します。Python は字句変数を完全にサポートしていないため、n の値を保持するデータ構造を作成する必要があります。また、Python には関数データ型がありますが、リテラル表現はありません (本体が単一の式である場合を除く)。そのため、返す名前付き関数を作成する必要があります。最終的には次のようになります。
def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar
Pythonユーザーは、なぜ単に書かないのかと疑問に思うかもしれません。
def foo(n): return lambda i: return n += i
あるいは
def foo(n): lambda i: n += i
そして、おそらくいつかそうなるだろうと私は推測します。(しかし、Python が Lisp へと完全に進化するのを待ちたくないのであれば、いつでも...)
OO 言語では、1 つのメソッドと、囲みスコープの各変数を置き換えるフィールドを持つクラスを定義することで、限定された範囲でクロージャ (囲みスコープで定義された変数を参照する関数) をシミュレートできます。これにより、プログラマーは、レキシカル スコープを完全にサポートする言語でコンパイラーによって実行されるようなコード分析を実行することになります。複数の関数が同じ変数を参照する場合は機能しませんが、このような単純なケースでは十分です。
Pythonの専門家は、これがPythonで問題を解決する好ましい方法であることに同意しているようです。
def foo(n): class acc: def __init__(self, s): self.s = s def inc(self, i): self.s += i return self.s return acc(n).inc
または
class foo: def __init__(self, n): self.n = n def __call__(self, i): self.n += i return self.n
Python 支持者に私が言語を誤解していると言わせたくないので、これらを含めましたが、どちらも最初のバージョンよりも複雑に思えます。同じことをしていて、アキュムレータを保持するための別の場所を設定しています。これは、リストの先頭ではなく、オブジェクト内のフィールドにすぎません。また、これらの特別な予約済みフィールド名、特にcallの使用は、少しハックのように思えます。
Perl と Python の競争において、Python ハッカーは、Python は Perl よりもエレガントな代替手段であると主張しているようですが、この事例が示しているのは、パワーこそが究極のエレガンスであるということです。つまり、構文が少々醜くても、Perl プログラムはよりシンプル (要素が少ない) です。
他の言語ではどうでしょうか? この講演で言及した他の言語 (Fortran、C、C++、Java、Visual Basic) では、この問題を実際に解決できるかどうかは明らかではありません。Ken Anderson は、次のコードが Java で実現できる最も近いものだと言っています。
public interface Inttoint { public int call(int i); }
public static Inttoint foo(final int n) { return new Inttoint() { int s = n; public int call(int i) { s = s + i; return s; }}; }
これは整数に対してのみ機能するため、仕様を満たしていません。Java ハッカーと何度もメールをやり取りした結果、前述の例のように動作する適切なポリモーフィック バージョンを作成するのは、非常に扱いにくいか不可能かの中間くらいの難しさであると言えます。誰かが作成したいのであれば、ぜひ見てみたいのですが、個人的には時間が足りません。
もちろん、他の言語ではこの問題を解けないというのは文字通り真実ではありません。これらすべての言語がチューリングと同等であるという事実は、厳密に言えば、どの言語でも任意のプログラムを書くことができることを意味します。では、どうすればよいでしょうか。極限の場合、それほど強力ではない言語で Lisp インタープリタを書くことになります。
冗談のように聞こえますが、大規模なプログラミング プロジェクトでは程度の差はあれ、このようなことが頻繁に起こるため、この現象には「グリーンスパンの第 10 ルール」という名前が付けられています。
十分に複雑な C または Fortran プログラムには、Common Lisp の半分の、アドホックで非公式に指定された、バグだらけで遅い実装が含まれています。
難しい問題を解決しようとする場合、問題は十分に強力な言語を使用するかどうかではなく、(a) 強力な言語を使用するか、(b) 強力な言語のデファクト インタープリタを作成するか、(c) 自分自身が強力な言語の人間コンパイラになるかです。これは、Python の例ではすでに起こり始めており、実際には、字句変数を実装するためにコンパイラが生成するコードをシミュレートしています。
この習慣は一般的であるだけでなく、制度化されています。たとえば、OO の世界では、「パターン」についてよく耳にします。これらのパターンは、ケース (c)、つまり人間のコンパイラが機能している証拠ではないでしょうか。プログラムにパターンが見られると、私はそれが問題の兆候であると考えます。プログラムの形状は、解決する必要のある問題のみを反映する必要があります。コード内のその他の規則性は、少なくとも私にとっては、十分に強力でない抽象化を使用していることの兆候です。多くの場合、記述する必要のあるマクロの展開を手動で生成しています。
注記
IBM 704 CPU は冷蔵庫ほどの大きさでしたが、かなり重かったです。CPU の重さは 3,150 ポンド、4K の RAM は別の箱に入っていて、さらに 4,000 ポンドの重さがありました。家庭用冷蔵庫の中で最大のものの 1 つである Sub-Zero 690 の重さは 656 ポンドです。
スティーブ・ラッセルは 1962 年に最初の (デジタル) コンピュータ ゲーム「Spacewar」も作成しました。
尖った髪の上司を騙して Lisp でソフトウェアを書かせたいなら、XML だと言ってみてもいいでしょう。
他の Lisp 方言での累積器ジェネレータは次のとおりです。
Scheme: (define (foo n) (lambda (i) (set! n (+ ni)) n)) Goo: (df foo (n) (op incf n _))) Arc: (def foo (n) [++ n _])
JPL における「業界のベストプラクティス」に関する Erann Gat 氏の悲しい話に触発されて、私は一般的に誤って適用されるこのフレーズについて取り上げることにしました。
Peter Norvig は、デザイン パターンの 23 個のパターンのうち 16 個が Lisp では「目に見えないか、より単純」であることを発見しました。
さまざまな言語に関する私の質問に答えてくれたり、この文書の草稿を読んでくれた多くの方々に感謝します。Ken Anderson、Trevor Blackwell、Erann Gat、Dan Giffin、Sarah Harlin、Jeremy Hylton、Robert Morris、Peter Norvig、Guy Steele、Anton van Straaten などです。彼らは表明された意見について一切責任を負いません。
関連している:
この講演には多くの人が反応したので、彼らが提起した問題に対処するために、 Re: Revenge of the Nerds という追加ページを作成しました。
また、 LL1メーリング リストで広範囲かつ有益な議論が巻き起こりました。特に、セマンティック圧縮に関する Anton van Straaten のメールを参照してください。
LL1 に関するメールのいくつかがきっかけで、私は『簡潔さは力である』における言語の力という主題についてさらに深く考えてみることにしました。
アキュムレータ ジェネレータ ベンチマークの標準的な実装のより大きなセットは、専用のページにまとめられています。