八発白中

技術ブログ、改め雑記

あなたがLispを無視することができない理由

(この記事はLisp Advent Calendar 1日目のためのエントリです。)

禅が好んで用いる比喩がある。月を指すには指が必要である。だが、その指を月と思う者はわざわいなるかな。
鈴木大拙「禅」

これをLispに例えるなら、こう言うことができるかもしれない。

Lispを書くには括弧が必要である。だが、その括弧をLispと思う者はわざわいなるかな。

Lispを普段書いている身としてはLispについて括弧がどう、というのは些細なものに思えるが、Lispを知らない人からすると自然な考えだろう。人間は知らないものを理解しようとするとき、自分が今まで見たもの、知っているものと比較して手がかりを得ようとする。Lispが他のプログラム言語と比較してユニークなものは、やはりその括弧で表現されたS式だ。

しかし、Lispが括弧を使った奇妙な構文を用いるのは理由がある。そしてそれがLispの強力さを生み、Lispは今まで生き残ってきた。

ここでその強力さを見せられるといいんだけど、それは他のプログラム言語とはあまりに異なるため学ぶのが難しい。しかし、本質的にはシンプルなことだ。その類似性から、しばしばそれは「悟り」と呼ばれている。

Lispの本質は、そのコード自身をLispのデータ構造で表すことができることだ。

これは主に、Lispプログラムを生成するLispプログラムを書くようなときに効果を発揮する。そしてそのようなプログラムをLispでは「マクロ」と呼ぶ。

マクロで何ができるか

Lispを学び、マクロが何であり、何ができるかを学んだ人には2通りの反応が見られる。好むか、恐れるか。

マクロをこの上なく好み、これ無しでプログラムを書くなんて不安でしかない、という人々と、逆に強力すぎるから言語機能としてあるべきではないという人々がいる。

これはLisp界でも見解が分かれるところで、特にSchemeの人々は後者の見方をする人も多いようだ。ただ一致しているのは、それが強力であることだ。あまりに強力なため、誰もそれを無視することはできない。

Lispプログラマにとって、なぜマクロはそんなに重要なのか? 単にマクロが提供するシンタックス上の便利さのためではなく、マクロはプログラムを操作するプログラムであり、そのことは常にLispコミュニティの中心的テーマであり続けているからである。FORTRANが数を、Cが文字とポインタをこき使う言語であるなら、Lispは、プログラムをこき使う言語である。そのデータ構造は、プログラムテキストを表現や操作するために有用である。マクロは、メタ言語で書かれたプログラムの最も直接の例である。Lispはそれ自身のメタ言語であるので、プログラミング言語全体の力が、プログラムテキストを変形する仕事を行なうために用いられ得る。

— Guy Steel Jr. and Richard P. Gabriel 「Lispの進化」

プログラムを生成するプログラムが必要になるのは、言語自身を拡張したいときだ。

たとえば、JavaScriptwith文というものがある。

with (obj) {
    a = b;
}

上のコードは以下のコードのいずれかと等価である。

a = b;
a = obj.b;
obj.a = b;
obj.a = obj.b;

つまり、ブロック内に出てくる変数の名前と同じ名前のobjのフィールドがあるときはobjのフィールドとして扱い、そうでない場合は単なる変数として扱う。

withは明らかにLispの影響を受けたものだが、一般的なJavaScriptコードではあまり使われていないようだ。「JavaScript: The Good Parts 」ではこの構文を「悪いパーツ」として使うべきではないと言っている。理由はそれが非常に効率が悪く、変数が何を表しているかを曖昧にするからだ。ただ、その理由は思想が悪いというよりも実装が悪いせいだと思う。

Common Lispにはそれと似たものとしてwith-slotsがある。オブジェクトのスロット名をそのまま変数 (CLではシンボル) に束縛する構文だ。

(defstruct person
  name age)

(defvar me (make-person :name "Eitarow Fukamachi" :age 26))

(with-slots (name age) me
  (format t "I'm ~A. ~D years old." name age))
;-> I'm Eitarow Fukamachi. 26 years old.

Common Lispではwith-slots内で使うオブジェクトのフィールド名を宣言する。宣言したフィールドは有効範囲内でまるで変数のように扱うことができるようになる。

尚この変数はオブジェクトに紐付けられているため、破壊的な変更もすることができる。

(person-age me)
;=> 26

(with-slots (age) me
  (incf age))

(person-age me)
;=> 27

JavaScriptwithがあまり使われない一方、Common Lispwith-slotsが使われる理由は、それが効率が良いからだ。with-slotsが行う変数とオブジェクトの紐付けはコンパイル時にすべて解決される。JavaScriptwithが効率が悪いのは、実行時にならないと変数がオブジェクトのフィールドなのかただの変数なのか解決できないところにある。

with-slotsCommon Lispの言語仕様に含まれるが、もし含まれないとしてもシンボルマクロを使って簡単に実装することができる。一方でPerlにこの構文を導入するのは非常に難しいだろう。

たとえばPerl風に書くと以下のようになるかもしれない。

sub with {
    my ($names, $object, $block) = @_;
    $block->(map { $object->{$_} } @$names );
}

my $person = { name => "Eitarow Fukamachi", age => 26 };

with(['name', 'age'], $person, sub {
    my ($name, $age) = @_;

    printf "I'm %s. %d years old.", $name, $age;
});

ただ、これは破壊的な操作をすることができないという点でJavaScriptwithCommon Lispwith-slotsに劣る。完全な実装をするとしたら、やはりPerl構文解析が必要だ。

好むにしろ嫌うにしろ、誰もLispを無視することができない理由はここにある。通常の言語ならwithを言語仕様に追加するところを、Lispはそれを仕様に追加することなく追加する機能を持つことで回避している。

Perlにできないが、Rubyにはできる、というようなものは言語レベルでは無い。しかし、PerlRubyにはできないが、Lispにはできるというものはある。マクロだ。Rubyはその構文により柔軟なDSLを作ることができる点でいくつかの問題をわかりづらくしてはいるが、それはプログラムの性能を引き換えにしている。Lispではコンパイル時と実行時の分離が簡単に書き分けられるため、性能面での劣化を心配せずに言語を拡張することができる。

言語の代替と進化

with-slotsは言語を少しだけ前進させる一例だったが、JavaScriptにはもう少し大きな視点での言語の進化にも例がある。

JavaScriptがWebアプリケーションで一般的に使われるようになったのも記憶に新しいが、最近では言語としてのJavaScriptではなく、JavaScriptに変換されるような拡張言語が多く出てきて実用化され始めている。

その代表はCoffeeScriptだ。CoffeeScriptはコンパイラとして実装されており、CoffeeScriptプログラムは完全にJavaScriptに変換される。そのコンパイラJavaScriptで実装されている。実行時には変換結果のJavaScriptが使われるため、実行時のオーバーヘッドも無い。

面白いことに、これはまったくLispのマクロを説明したような文章じゃないか。LispのマクロはLispに変換されるLispコンパイラと考えることができる。コンパイラLispで実装されており、実行時のオーバーヘッドは無い。

ただし、JavaScriptにはJavaScriptへのコンパイラを書く機能が言語仕様に含まれないため、CoffeeScriptは別のプログラムとして提供されている点は異なる。

コンパイラを書く機能が言語仕様に含まれるのと含まれないのではどういった違いがあるだろう。言語仕様に含まれないことで蒙るデメリットは何か。

いや、Lispのようにコンパイラを書く機能が言語に含まれているのはプログラム言語としては特異なのだから、むしろLispが持つメリットと言うべきかもしれない。実を言うとLispは、単なるプログラム言語ではない。Paul Grahamがそれについて言及している。

Lispプログラミング言語として設計されたんじゃなかった。少なくとも我々がこんにち使うプログラミング言語と言う意味では。つまりコンピューターに何をすべきかを指示するもの、という意味ではね。McCarthyはその後確かにそういう意味でのプログラミング言語を作ろうとしたけど、こんにちのLispになったものは彼が理論的な実験としてやったもの、チューリングマシンのより便利な代替物を定義しようとした試みの結果なんだ。
Paul Graham「技術野郎の復讐」

問題はCoffeeScriptをさらに拡張する方法が無いことだ。なぜならCoffeeScriptにマクロが無いから。CoffeeScriptよりももっと上位の言語が欲しくなったとき、CoffeeScriptと同じアプローチ、つまりCoffeScriptにコンパイルするプログラムを書く必要がある。

CoffeeScriptを例に出したが、これは他のどのJS代替言語にも言えることだ。その言語が時代に追いつけず進化が止まったとき、その言語は死ぬしかない。

生き残る言語

ある言語でプログラムを書いているとき、その言語がどう生まれてどのように進化し、将来どうなっているかまで想像してプログラムすることは無いかもしれない。しかし、過去を振り返るとそれは誰にとっても無縁ではいられない。

Perl 4は1991年にリリースされたが、1994年には5.0がリリースされた。言語としての互換性は保ってはあるが推奨される構文は変わり、その後Perl 4でプログラムを書く利点はまったくなくなった。Perl 4の寿命は3年しかなかったことになる。

Ruby 1.8は2003年にリリースされたが2007年には互換性の無い1.9がリリースされた。そして2013年の今では非推奨になっている。Ruby 1.8の寿命は長く見たとしても10年だった。

数年後はどうだろう。Ruby 1.9を使っているだろうか。僕は確信が持てない。

これらの原因は、その言語仕様に言語自身が成長する機能が十分についていないからだと僕は思う。そのため機能追加するために仕様が追加されたり、誤った抽象化だったことが判明したときに修正する必要が出てくる。プログラムに仕様変更はよくあることではあるが、プログラム言語がそうころころと仕様を変更していては影響範囲が大きすぎる。

この仮説はLisp方言の一つであるCommon Lispの例にも合致する。Common Lispは1984年に第1版の仕様が策定され、6年後の1990年に第2版が出た。ただしこの改訂はCLOS(オブジェクトシステム)や拡張loopマクロなど、Common Lisp上で既に実装されていたものを仕様に含めただけだった。それからは20年以上改訂されずに今も使われ続けている*1

もちろん、Lispに足りないものもいくつかある。ライブラリは他の言語よりは少なめだし、コミュニティの活発度も比較するとあまり大きいとは言えない。けれど、言語自身に欠陥があるのに比べればまったく些細な問題だと僕は思う。

Lispの括弧はその強力さの結果として見えているだけだ。月を見たいときに重要なのは指ではなく、月であるということを忘れないで欲しい。

実践Common Lisp

実践Common Lisp

Land of Lisp

Land of Lisp

*1:Schemeは言語仕様が定期的に改訂されているが、それはSchemeCommon Lispとは別の思想により設計されていてまだ未完成だからだ。