ネーズの復讐
Original2002年5月
「私たちはC++プログラマーを狙っていました。私たちはそのほとんどをLispの半分まで引きずり下ろすことができました」
- Javaの共同作者であるGuy Steele
ソフトウェアビジネスでは、尖った頭の学者と、もう一つの強大な力である尖った頭の上司との間で絶え間ない闘いが行われています。尖った頭の上司がどんな人物かは、皆さんご存知ですよね? 私は、テクノロジー界の大半の人々が、このカートゥーン・キャラクターを認識しているだけでなく、自社にいる実際の人物がそのモデルになっていることも知っていると思います。
尖った頭の上司は、単独では一般的な2つの特質を奇跡的に組み合わせています。 (a) テクノロジーについて全く知識がない (b) それについて非常に強い意見を持っている
例えば、ソフトウェアを書く必要があるとしましょう。尖った頭の上司はその ソフトウェアがどのように機能しなければならないかわかりませんし、プログラミング言語を 区別することもできません。にもかかわらず、どの言語で書くべきかを知っています。 まさにそうです。彼はJavaで書くべきだと考えています。
なぜそう考えるのでしょうか? 尖った頭の上司の頭の中を覗いてみましょう。 彼の考えは次のようなものです。Javaは標準です。報道で絶えずそれについて 聞くので、標準であることがわかります。それが標準なら、使うのに問題は ないでしょう。そして、今の私の従業員が辞めても、プログラマーは いつも不思議なことに辞めていくのですが、簡単に 置き換えられるJavaプログラマーがたくさんいるはずです。
これは、そう無理のない考えに聞こえます。しかし、それは1つの 言外の前提に基づいています。そしてその前提が間違っていることが わかります。尖った頭の上司は、すべてのプログラミング言語が ほぼ同等だと信じています。
それが真実なら、彼は的を射ています。言語が全て同等なら、 他の人が使っているものを使えばいいに決まっています。
しかし、すべての言語が同等ではありません。そして、私はそれを 証明することができます。言語の違いに立ち入らなくても証明できます。 1992年に尖った頭の上司に、どの言語でソフトウェアを書くべきかを 聞いたら、今日と同じくらい迷いなく答えたでしょう。C++で書くべきだと。 しかし、言語が全て同等なら、尖った頭の上司の意見はなぜ変わるべきですか? 実際、Javaの開発者がなぜ新しい言語を作る必要があったのでしょうか?
新しい言語を作るのは、既存のものよりも何らかの点で優れていると 考えているからに違いありません。そして実際、Goslingは最初のJava ホワイトペーパーで、JavaがC++の問題を解決するために設計されたと 明確に述べています。 つまり、言語は全て同等ではないのです。 尖った頭の上司の頭の中を辿っていくと、Javaに行き着き、 さらにJavaの歴史とその起源に遡っていくと、最初の前提と矛盾する 考えに行き着くのです。
では、誰が正しいのでしょうか? James Goslingか、尖った頭の上司か? 驚くことではありませんが、Goslingが正しいのです。ある問題には 特定の言語の方が他の言語よりも優れているのです。そして、これは 興味深い問題を提起します。Javaは特定の問題ではC++よりも優れるよう に設計されました。どのような問題でしょうか? Javaはいつ優れ、 C++はいつ優れているのでしょうか? 他の言語がそれらよりも優れている 状況はあるのでしょうか?
この問題を考え始めると、本当に複雑な問題に突入することになります。 尖った頭の上司がこの問題の全体像を考えなければならないとしたら、 頭が爆発してしまうでしょう。すべての言語が同等だと考えている限り、 勢いのある言語を選べばいいだけです。それは技術ではなく流行の問題 なので、彼でも正解を見つけられるかもしれません。 しかし、言語に違いがあるとなると、彼の知らないことについて2つの 方程式を同時に解かなければなりません。つまり、解決すべき問題に 対する20種類以上の主要な言語の相対的な適合性と、各言語について プログラマーやライブラリなどを見つける可能性のバランスを見つけ出す 必要があります。 そこに待っているのがこの問題の本質なら、尖った頭の上司はそのドアを 開けたくないのも無理はありません。
すべてのプログラミング言語が同等だと信じることの欠点は、それが真実で はないということです。しかし、その利点は生活がずっと簡単になることです。 そして、この考え方が広く受け入れられている主な理由がそこにあると 思います。それは快適な考え方なのです。
Javaは素晴らしい言語に違いないと思います。なぜなら、それは最新の プログラミング言語だからです。あるいは、そうなのでしょうか? プログラミング 言語の世界を遠くから見ると、Javaが最新のものに見えます。(十分遠くから 見れば、Sunが支払った大きな看板しか見えません) しかし、この世界を 近くで見ると、クールさにも程度があることがわかります。ハッカー サブカルチャーの中では、Javaよりもはるかにクールな言語があります。 それがPerlです。例えばSlashdotはPerlで生成されています。あの人たちが Java Server Pagesを使うとは思えません。しかし、さらに新しい言語 Pythonがあり、その使い手はPerlを軽んじる傾向にあります。 さらに待っているものもあります。
これらの言語を順に見ていくと、Java、Perl、Pythonと、興味深いパターンが 見えてきます。少なくとも、Lispハッカーの視点からはそうです。 それぞれ順にLispに近づいているのです。Pythonは多くのLispハッカーが 間違いだと考えている機能さえコピーしています。簡単なLispプログラムを Pythonに1行ずつ翻訳することができます。2002年になって、ついにプログラミング 言語は1958年の水準に追いついてきたのです。
数学に追いつく
つまり、Lispは1958年にJohn McCarthyによって最初に発見されたのですが、 一般的なプログラミング言語はついにその時の考えに追いついてきたということです。
しかし、それはどうしてでしょうか? コンピューター技術は非常に 急速に変化するものではないのですか? 1958年当時、コンピューターは 冷蔵庫ほどの大きさで、腕時計並みの処理能力しかありませんでした。 そんな昔の技術がどうして今でも関連性があり、むしろ最新の 開発よりも優れているというのでしょうか?
私がどうやってそうするかを説明しましょう。それは、Lispが今日私たちが意味するプログラミング言語としては本当に設計されていなかったからです。プログラミング言語とは、コンピューターに何をするよう指示するものです。McCarthyは最終的にこの意味でのプログラミング言語を開発する意図がありましたが、私たちが実際に手に入れたLispは、彼が別個に行った理論的な演習に基づいていました。つまり、チューリングマシンよりも便利な代替案を定義する取り組みです。McCarthyは後に次のように述べています。
Lispがチューリングマシンよりもすっきりしていることを示す別の方法は、ユニバーサルなLisp関数を書いて、それがユニバーサルなチューリングマシンの説明よりも簡潔で理解しやすいことを示すことでした。 それがevalというLisp関数でした。これはLisp式の値を計算します。 evalを書くには、Lisp関数をLispデータとして表現する記法を発明する必要がありました。そしてそのような記法は、論文の目的のために考案されたもので、Lispプログラムを実際に表現するために使われるとは思われていませんでした。
次に起こったことは、1958年の終わりごろ、McCarthyの大学院生の1人であるSteve Russellが、この evalの定義を見て、それをマシン語に翻訳すれば、Lispインタープリターになるだろうと気づいたということです。
これは当時大きな驚きでした。McCarthyはインタビューでこう述べています。
Steve Russellが「えっ、私がこのevalを...プログラムしてみましょうか」と言ったので、私は「ほほう、理論と実践を混同しているな、このevalは読むためのものであって、計算するためのものではない」と言いました。でも彼は実行してしまいました。つまり、私の論文のevalを[IBM] 704のマシンコードにコンパイルし、バグを修正して、それをLispインタープリターとして宣伝したのです。そこでLispは、今日の形態を基本的に持つことになったのです。
こうして、数週間のうちに、McCarthyの理論的な演習がまさかの実際のプログラミング言語に変貌したのです。しかもそれは、彼が意図していたよりもはるかに強力なものでした。
だからこの1950年代の言語が時代遅れにならない理由は、それが技術ではなく数学だからです。数学は古くならないのです。Lispを比較すべきなのは1950年代のハードウェアではなく、1960年に発見されたクイックソートアルゴリズムのようなものです。
1950年代から生き残っている別の言語にFortranがありますが、それは言語設計の正反対のアプローチを表しています。Lispは理論の一部が思わぬことにプログラミング言語になったのに対し、Fortranは当初からプログラミング言語として意図的に開発されたものの、今から見れば非常に低レベルなものでした。
Fortran I、つまり1956年に開発された言語は、現代のFortranとは全く異なる動物でした。Fortran Iはほとんどアセンブリ言語に数学を加えたものでした。最近のアセンブリ言語よりも、むしろ機能が少ない面もありました。サブルーチンがなく、ただブランチがあるだけでした。現代のFortranは、むしろLispに近づいているといえます。
Lispとfortranは、数学に根ざした1つの系統と、マシンアーキテクチャに根ざした別の系統の2つの進化の木の幹でした。この2つの木は、ずっと収束し続けてきました。Lispは当初から強力で、その後20年間で高速化しました。いわゆるメインストリームの言語は当初は高速でしたが、その後40年かけてより強力になり、今では最先端のものはかなりLispに近づいています。 近づいてはいますが、まだ数点欠けているものがあります。
Lispが異なっていたもの
Lispが最初に開発された時、9つの新しいアイデアを体現していました。これらのうち、いくつかは今では当然のように受け入れられていますが、より高度な言語でしか見られないものもあり、2つはLispにしか存在しません。これらの9つのアイデアを、メインストリームに採用された順に示します。
-
条件分岐。条件分岐はif-then-elseの構造です。今では当然のように受け入れられていますが、Fortran Iにはありませんでした。Fortran Iにはマシン命令に密接に基づいた条件付きのgotoしかありませんでした。
-
関数型。Lispでは、関数は整数や文字列と同じデータ型です。リテラル表現を持ち、変数に格納したり、引数として渡したりできます。
-
再帰。Lispが最初にサポートしたプログラミング言語でした。
-
動的型付け。Lispでは、すべての変数は実質的にポインターです。型を持つのは値であって、変数ではありません。変数への代入や束縛は、ポインターのコピーを意味するだけです。
-
ガベージコレクション。
-
式からなるプログラム。Lispのプログラムは、それぞれが値を返す式の木構造です。これは、Fortranやその後の多くの言語が式と文を区別しているのとは対照的です。
Fortran Iでこの区別が必要だったのは、文をネストできなかったからです。数学の計算には式が必要でしたが、それ以外に値を返す必要はありませんでした。
ブロック構造言語の登場で、この制限は消えましたが、その時にはもう遅すぎでした。式と文の区別は根付いてしまっていて、Fortranからalgolに、そしてそれらの子孫言語にも広がっていきました。
-
シンボル型。シンボルは、ハッシュテーブルに格納された文字列へのポインターです。ポインターを比較すれば、文字列を1文字ずつ比較する必要がありません。
-
記号と定数からなる木構造のコード表記。
-
常に存在する全体としての言語。読み取り時、コンパイル時、実行時の区別がほとんどありません。読み取り時にコードを実行したり、コンパイル時にコードを読んだり、実行時にコードをコンパイルしたりできます。
読み取り時にコードを実行すれば、Lispの構文をユーザーが書き換えられます。コンパイル時にコードを実行すればマクロの基礎になります。実行時にコードをコンパイルすれば、Emacs のようなプログラムの拡張言語としてLispを使えます。そして、実行時に読み取れば、s式を使ってプログラム間で通信できます。これは最近XMLとして再発明されたアイデアです。
Lispが初めて登場した時、これらのアイデアは通常のプログラミング実践から非常に遠いものでした。当時のハードウェアに大きく影響されていたのです。 時間とともに、主要な言語に体現された「デフォルトの言語」は、徐々にLispに近づいてきました。1-5のアイデアは今や広く受け入れられています。6番目のアイデアも主流に現れ始めています。Pythonには7番目のアイデアの形態があるものの、それを表す構文はないようです。
8番目のアイデアが最も興味深いかもしれません。アイデア8と9は、スティーブ・ラッセルが実装したものが、マッカーシーが意図していなかったものだったため、たまたまLispの一部になりました。しかし、これらのアイデアは、Lispの奇妙な外観と最も特徴的な機能の両方を生み出しています。Lispが奇妙に見えるのは、奇妙な構文があるからではなく、構文がないからです。他の言語でパースされるときに構築されるパーストリーを直接表現し、これらのツリーはリストで構成されており、これがLispのデータ構造です。
自身のデータ構造で言語を表現することは非常に強力な機能です。アイデア8と9を合わせると、プログラムを書くプログラムを書くことができます。これは奇妙に聞こえるかもしれませんが、Lispでは日常的なことです。これを行う最も一般的な方法は、マクロと呼ばれるものを使うことです。
Lispにおける「マクロ」という用語は、他の言語における意味とは異なります。Lispのマクロは、略語から新しい言語のコンパイラまで、さまざまなものを指します。Lispを本当に理解したい、あるいはプログラミングの視野を広げたい場合は、マクロについてさらに学ぶことをお勧めします。
Lispのマクロは、私の知る限り、Lispにしか存在しません。これは部分的に、マクロを持つには、Lispのように奇妙に見える言語を作る必要があるためです。また、最終的な力を加えると、新しい言語を発明したのではなく、Lispの新しい方言を作ったにすぎないと主張できなくなるためかもしれません。
これは主に冗談として述べていますが、まったく真実です。car、cdr、cons、quote、cond、atom、eqと関数を表すリストの表記法を定義すれば、Lispの残りの部分を全て構築できます。これがまさにLispの定義する特質なのです。これがLispの形状を決めた理由です。
言語が重要な場所
では、Lispが主流の言語が漸近的に近づいている種類の限界を表しているとすれば、それを使ってソフトウェアを書くべきでしょうか? より弱い言語を使うことで何を失うのでしょうか? 時には、最先端の革新に立っていないほうが賢明ではないでしょうか? 人気は、ある程度自己正当化するものではないでしょうか? たとえば、プログラマを簡単に雇えるような言語を使いたがるポインティヘアードボスは、正しいのでしょうか?
もちろん、プログラミング言語の選択があまり重要でない プロジェクトもあります。一般的に、アプリケーションがより要求の高いものであるほど、強力な言語を使うことでより大きな効果が得られます。しかし、多くのプロジェクトは全く要求が高くありません。 ほとんどのプログラミングは、小さなグルーピングプログラムを書くことで構成されており、そのようなグルーピングプログラムには、既に慣れ親しんでいて、必要なことをするためのライブラリがよいWindows アプリがあれば、どの言語でも使えます。Windowsアプリからデータを別のアプリに渡す必要がある場合は、Visual Basicを使えばよいでしょう。
Lispでも小さなグルーピングプログラムを書くことはできます(デスクトップ電卓として使っています)が、Lispのような言語の最大の利点は、複雑なプログラムを書いて、激しい競争の中で難しい問題を解決する必要がある場合です。 良い例が、Orbitzにライセンスを供与しているITAソフトウェアの航空券検索プログラムです。 この会社は、既に2つの大手競合企業、TravelocityとExpediaが支配している市場に参入し、技術的に彼らを圧倒しているようです。
ITAのアプリケーションの中核は20万行のCommon Lispプログラムで、競合他社が依然としてメインフレーム時代のプログラミング手法を使っているのに比べ、はるかに多くの可能性を検索しています。 (ITAも一種のメインフレーム時代のプログラミング言語を使っているのですが。) 私はITAのコードを一切見たことはありませんが、トップハッカーの1人によると、多くのマクロを使っているそうで、それを聞いて驚きませんでした。
遠心力
私は、一般的でない技術を使うことにコストがないと言っているわけではありません。ポインティヘアードボスが完全に間違っているわけではありません。しかし、彼はリスクを理解していないため、それらを過大評価する傾向にあります。
一般的でない言語を使うことで生じる可能性のある3つの問題を考えられます。プログラムが他の言語で書かれたプログラムと上手く連携しないかもしれません。利用可能なライブラリが少ないかもしれません。プログラマを雇うのが難しくなるかもしれません。
これらの問題がどの程度重要かは、状況によって異なります。最初の問題の重要性は、システム全体を制御しているかどうかによって変わります。リモートユーザーのマシンで動作するアプリケーションを書く場合(名前は挙げませんが)、OSと同じ言語で書くことに利点があるかもしれません。しかし、ITAのように、すべての部品のソースコードを持ち、システム全体を制御できる場合は、好きな言語を使えます。互換性の問題が生じても、自分で修正できます。
サーバーベースのアプリケーションでは、最先端の技術を使うことができ、これが Jonathan Ericksonが「プログラミング言語の復興」と呼ぶものの主な原因だと思います。PerlやPythonのような新しい言語について話題になるのは、Windowsアプリを書くために使われているからではなく、サーバー上で使われているからです。そして、ソフトウェアがデスクトップからサーバーに移行していくにつれ(Microsoftも諦めているようですが)、中道的な技術を使う圧力は減少していくでしょう。
ライブラリの重要性も、アプリケーションによって異なります。要求の少ない問題では、言語の本質的な力よりも、利用可能なライブラリの存在が重要になります。どこがその分岐点なのでしょうか? 正確に言うのは難しいですが、それは間違いなく、アプリケーションと呼べるものよりも低い水準にあります。企業がソフトウェアビジネスの一部と見なし、製品の1つとなるアプリケーションを書く場合は、数人のハッカーが6か月以上かけて書くプロジェクトになるでしょう。そのようなプロジェクトでは、強力な言語の方が、既存のライブラリの便利さを上回ると思われます。
ポインティヘアードボスの3番目の懸念、プログラマーを雇うことの難しさは、赤い魚の骨だと思います。結局のところ、何人のハッカーを雇う必要があるのでしょうか? 今では、ソフトウェアは10人未満のチームで最も効果的に開発されることを誰もが知っているはずです。そのような規模のハッカーを、誰もが聞いたことのある言語で雇うのは難しくないはずです。もしリスプのハッカーを10人見つけられないなら、おそらくあなたの会社は間違った場所に拠点を置いているのでしょう。
実際、より強力な言語を選択すれば、必要なチームの規模を小さくできる可能性があります。なぜなら、(a)より強力な言語を使えば、必要なハッカーの数が少なくて済むでしょうし、(b)より高度な言語で働くハッカーはおそらりもっと賢明だからです。
「標準」と見なされる技術を使うよう強い圧力がかかるのは事実だと言っています。Viaweb(現在のYahoo Store)では、VCや潜在的な買収者にリスプを使っていることで眉をひそめられました。しかし、Intelのジェネリックなボックスをサーバーとして使ったり、FreeBSDという当時はまだ無名のオープンソースのUnixバリアントを使ったり、SET(http://news.com.com/2100-1017-225723.html)という誰も覚えていないeコマース規格を無視したりしたことでも眉をひそめられました。
スーツの人間に技術的な決定をさせてはいけません。私たちがリスプを使ったことで、一部の買収者を少し驚かせたのは事実です。しかし、リスプを使っていなければ、彼らが私たちを買収したくなるようなソフトウェアを書くことはできなかったでしょう。彼らにとっては異常に見えたことが、実は原因と結果だったのです。
スタートアップを立ち上げるなら、VCや潜在的な買収者を喜ばせるようにプロダクトを設計してはいけません。ユーザーを喜ばせるようにプロダクトを設計してください。ユーザーに勝てば、あとは全て付いてきます。そうでなければ、あなたの技術選択がどれほど安心できるものだったかなど、誰も気にしないでしょう。
平凡さのコスト
より弱い言語を使うことで、どれほどのものを失うのでしょうか? この点についてのデータもいくつかあります。
おそらく最も便利な指標はコードサイズでしょう。高水準言語の目的は、より大きな抽象化、つまり大きなレンガを提供することで、壁を築くのに必要なレンガの数を減らすことです。 したがって、言語が強力であるほど、プログラムは短くなります(もちろん単なる文字数ではなく、個別の要素数です)。
より強力な言語によってどのようにプログラムを短く書くことができるのでしょうか? 言語がそれを許容してくれれば、ボトムアッププログラミングと呼ばれる手法を使うことができます。ベースの言語でアプリケーションを単純に書くのではなく、ベースの言語の上に、あなたのようなプログラムを書くための言語を構築し、それを使ってプログラムを書くのです。この組み合わせたコードは、ベースの言語だけで書いた場合に比べて大幅に短くなります。実際、ほとんどの圧縮アルゴリズムはこのようにして動作しています。ボトムアップのプログラムは、多くの場合、言語レイヤー自体を変更する必要がないため、より修正しやすいはずです。
コードサイズは重要です。なぜなら、プログラムを書くのにかかる時間は主にその長さに依存するからです。別の言語で書くと、プログラムが3倍長くなるとすれば、書くのに3倍の時間がかかるでしょう。そして、人を増やせばこれを回避できるわけではありません。ある一定の規模を超えると、新しい人員は実際にマイナスになってしまうのです。フレッド・ブルックスがその有名な著書『人月の神話』で述べたこの現象は、私が見てきたことすべてが裏付けています。
では、リスプで書いた場合、プログラムはどのくらい短くなるのでしょうか? リスプとCの比較で聞いた数字の多くは7-10倍ほどでした。しかし、『New Architect』誌の最近の記事では、「1行のリスプが20行のCに相当する」と述べられています。この記事にはITAの社長からの引用が多数あるので、この数字はITAから得たものだと考えられます。そうであれば、この数字を信頼できるでしょう。ITAのソフトウェアにはCやC++もかなり含まれているので、彼らは経験に基づいて語っているのです。
私の推測では、これらの倍率は一定ではないと思います。より難しい問題に直面したり、より優秀なプログラマーが関わったりすると、倍率は増加すると考えられます。本当に優秀なハッカーは、より良いツールからより多くを引き出すことができるのです。
とりあえず1つのデータポイントとして、ITAと競争し、Cでソフトウェアを書くことにした場合、ITAはあなたの20倍の速さで開発できるでしょう。あなたが1年かけて新機能を開発したとすれば、ITAはわずか3週間で同じものを複製できるでしょう。一方、ITAが3か月かけて新しいものを開発したとすれば、あなたがそれを手に入れるまでに5年もかかるかもしれません。
そしてご存知のとおり、これが最良のシナリオです。コードサイズの比率について話をする際、あなたはその弱い言語でプログラムを書くことができるという前提があります。しかし実際には、プログラマーにできることにも限界があります。低レベルの言語で難しい問題を解こうとすると、一度に頭の中に保持しなければならないことが多すぎるポイントに到達してしまうのです。
したがって、ITAが3か月でリスプで書けるものを、その想像上の競争相手が5年かけてやっと完成させられるかもしれない、と私が言ったとき、それは何も問題がなければの話です。実際の企業では、5年もかかるような開発プロジェクトは、完成することすらないでしょう。
これは極端な例だと認めます。ITAのハッカーは特に賢明で、Cはかなり低レベルの言語です。しかし、競争の激しい市場では、2倍や3倍の差でさえ、常に遅れ続けることを意味するでしょう。
レシピ
このような可能性は、ポインティヘアードボスは考えたくもないのです。そのため、ほとんどの人はそうしません。結局のところ、ポインティヘアードボスは自分の会社が負けても構わないのです。ただし、それが自分のせいだと証明されないようにすることが一番安全な道なのです。群れの中心に留まることが、彼にとって最も賢明な戦略なのです。
大規模組織内では、このアプローチを表す言葉は「業界のベストプラクティス」です。その目的は、ポインティーヘアードのボスを責任から守ることです。もし彼が「業界のベストプラクティス」を選択し、会社が損失を被った場合、彼は非難されることはありません。彼は選択したのではなく、業界が選択したのです。
私はこの用語が当初、会計方法などを記述するために使用されたと信じています。その意味するところは、大まかに言えば、奇抜なことはするなということです。会計においてはそれが良いアイデアかもしれません。「最先端」と「会計」という言葉は一緒に聞こえ良くありません。しかし、この基準をテクノロジーに関する決定に持ち込むと、間違った答えが得られるようになります。
テクノロジーは、しばしば最先端であるべきです。プログラミング言語においては、Erann Gatが指摘したように、「業界のベストプラクティス」が実際に得られるのは最良のものではなく、ただの平均的なものにすぎません。ある決定によってあなたのソフトウェア開発速度が、より積極的な競争相手の一部分になってしまった場合、「ベストプラクティス」は適切な言葉ではありません。
ですので、私は非常に価値があると思われる2つの情報があります。実際、私は自身の経験からそれを知っています。1つ目は、言語には力の差があるということです。2つ目は、ほとんどの管理者がこれを意図的に無視しているということです。これら2つの事実は、まさに金を稼ぐための処方箋です。ITAはこの処方箋が実行された例です。 ソフトウェアビジネスで勝ちたいのであれば、あなたができる最も難しい問題に取り組み、手に入る最も強力な言語を使い、競争相手のポインティーヘアードのボスが平均に回帰するのを待つだけです。
付録: 力
プログラミング言語の相対的な力についての私の意味するところを例示するために、次の問題を考えてみましょう。 累積器を生成する関数を書きたいと思います。これは、数値 n を受け取り、数値 i を受け取って n に i を加算した値を返す関数を返す関数です。
(加算ではなく、インクリメントです。累積器は蓄積しなければなりません。)
Common Lispでは、これは以下のようになります。
(defun foo (n)
(lambda (i) (incf n i)))
Perl 5では、
sub foo {
my ($n) = @_;
sub {$n += shift}
}
となり、Lispのバージョンよりも要素が多いのは、Perlでは手動でパラメータを抽出しなければならないためです。
SmalltalkのコードはLispよりもわずかに長くなります。
foo: n
|s|
s := n.
^[:i| s := s+i. ]
これは、一般的に字句変数は機能しますが、パラメータに代入することはできないため、新しい変数 s を作成する必要があるためです。
Javascriptの例は、再び、わずかに長くなります。これは、Javascriptが文と式の区別を保持しているため、値を返すには明示的な return ステートメントが必要になるためです。
function foo(n) {
return function (i) {
return n += i
}
}
(公平に言えば、Perlも同様に文と式の区別を保持していますが、典型的なPerlの方法で対処しています。つまり、return を省略できるようにしています。)
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がリスプに完全に進化するのを待つのを嫌なら、いつでも...)
オブジェクト指向言語では、クロージャ(囲む範囲で定義された変数を参照する関数)を限定的に模倣することができます。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はより優雅な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インタープリタを書くことです。
これは冗談のように聞こえますが、大規模なプログラミングプロジェクトでしばしば起こることで、この現象にはGreenspunの第10の法則という名称があります:
十分に複雑なCまたはFortranプログラムには、Common Lispの半分を実装したアドホックで非公式に指定されたバグ付きの遅い実装が含まれています。
難しい問題を解こうとすると、問題は強力な言語を使うかどうかではなく、(a)強力な言語を使うか、(b)事実上のインタープリタを書くか、(c)自分がコンパイラになるかです。 この傾向は、Pythonの例で既に始まっているのが見られます。ここでは、コンパイラが生成するコードを事実上シミュレーションしています。
この慣行は一般的であり、制度化されています。たとえば、OOの世界では「パターン」について多くを聞きます。これらのパターンは時には(c)の人間コンパイラの証拠ではないでしょうか。プログラムにパターンが見られるとき、私はトラブルの兆候だと考えます。プログラムの形状は、解決すべき問題のみを反映するべきです。コードのその他の規則性は、私にとっては、十分強力な抽象化を使っていないという兆候です。多くの場合、必要なマクロの展開を手作業で生成しているのです。
注記
IBM 704 CPUは冷蔵庫ほどの大きさでしたが、はるかに重かったです。CPUの重量は3150ポンド、4KのRAMは別の箱に入っており、さらに4000ポンドもありました。最大の家庭用冷蔵庫の1つであるSub-Zero 690は656ポンドです。
Steve Russellはまた、1962年に最初の(デジタル)コンピューターゲームであるSpacewarを書きました。
ポインティーヘアードのボスにLispで書けると言って許可を得るには、それがXMLだと言えばいいかもしれません。
他のLisp方言のアキュムレータジェネレータは以下の通りです:
Scheme: (define (foo n)
(lambda (i) (set! n (+ n i)) n))
Goo: (df foo (n) (op incf n _))
Arc: (def foo (n) [++ n _])
Erann Gatの「業界のベストプラクティス」に関する悲しい話は、この一般的に誤って適用されるフレーズに取り組むきっかけになりました。
Peter Norvigは、Design Patternsの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 でのメールの一部は、Succinctness is Powerで言語の力の問題をさらに深く掘り下げるきっかけになりました。
アキュムレータジェネレータベンチマークの正典的な実装の大きなセットが、専用のページにまとめられています。
Japanese Translation, Spanish Translation, Chinese Translation