[Common Lisp]JavaやC++、etcな人向けに説明するCommon Lispの利点

個人的なPractical Common Lisp(実践Common Lisp)を読んだ感想のサマリとして、JavaC++RubyPython等の言語の人などに説明できるように、Common Lispの利点をまとめます。間違いなどがあったら、ご指摘いただければうれしいです。

マクロ

Lisp以外の言語には追随できない最大の利点。http://user.ecc.u-tokyo.ac.jp/~tt076524/onlispjhtml/ 等、いろんなところで詳しく説明されている。

さらっと言ってしまえば、実行時にプログラムを生成するプログラムを書く機能。

すぐに思いつく利点は、保守性の大敵であるクローンコードをなくせること。他の言語だと、どんなにがんばっても同じようなコードを複数書かざるを得ない状況になるときがあるが、そんなときの最終手段がマクロだ。これがあれば何でもできる。マクロには他にも多くの利点があるが、説明するのが難しいので、割愛。上記サイト参照。「内部DSL」みたいな単語でググると、何かわかるかもしれない。

そんなことができる理由の主なものは2点。プログラム自体がネストされたリストデータとして扱える点。実行がRead-Compile-Evalという過程を経て行われる点。詳しく説明すると長くなるので、上記サイト参照。

クロージャ

端的に言うと、「関数ポインタ+環境(関数内からアクセスできる変数)」。

利点は、関数自体が変数のように代入したり引数として渡せたりする点。加えて、関数の中に関数が書け、そのときCの関数みたいにauto変数が消えたりはしない。
JavaC++だと、無理やりクラスを作らざるを得ない。Javaでいうsortに対するComparatorや、C++でいうSTLの関数オブジェクトを書く苦痛から開放される。
RubyPythonにはクロージャがあるので、その点でCommon Lispと同じだ。

クロージャの意味や原理、言語による違いを理解するには、関数ポインタ、スコープ、エクステントというキーワードについて調べればよい。ここにすばらしい説明がある→http://practical-scheme.net/docs/lambda-j.html

パッケージシステム

名前空間を提供する機能。イメージとしては、JavaPythonなどのパッケージや、C++のnamespaceと同じ。
ただし、アクセス権を制御する機能がクラスではなくパッケージにある。

クラス-インスタンスシステムとアクセス権を設定する機能が切り離されていることで、クラスのメンバだけでなく、グローバルな変数や関数に対してもアクセス権が設定できる。アクセス権の設定もシンプルで、外からアクセス可能(exportする)か、不可能(export)しないかの2つだけだ。それ以外は実質必要とならないと思われる。

アクセス権は、どちらかというとクラスシステムよりはパッケージシステムに所属するのが妥当のように思える。主に開発者間でのコードの局所化に使うためにあるからだ。

また、JavaC++のprivateみたいに、何が何でもアクセスできなくなっているわけではなく、規則を破ることも可能だ。ただし、規則を破っている箇所は、ソースコードを::(コロン2つ)でgrepすればよいので、すぐにリスクを判別できる。

さらに詳しい情報は、「シンボルのインターン」というキーワードで調べればよい。

コンディション

主にエラー処理をするための機能。JavaC++RubyPythonでは、「例外処理」といわれているものだ。

ただし、コンディションシステムは、例外処理のような「ガキの使い」ではない。例外処理では、throwされた時点で、「読み込み中のファイルの形式が不正だったので、先には進めません。全部キャンセル。終わり。(後始末だけはしておいたけどね)」と言ってcatch節のあるところまで強制的に巻き戻る。

それに対して、コンディションシステムでは、エラーが起こったという「通知」と、呼び出し元に戻る「巻き戻し」が分離されている。「読み込み中のファイルの行がおかしんだけど、どうする?この行を無視して処理を続ける?この行を空文字列とみなして処理を続ける?それとも処理を中断する?」と聞いて(通知して)くるのだ。呼び出し元は、提示された選択肢の中から、「じゃあ、その行だけ無視して処理を継続してくれ。」といって処理を再開(restart)できる。実に立派な大人の対応だ。

エラー処理に例外を使う言語でこういう柔軟な対応をしたければ、「データファイル読み込み関数(不正行を無視する版)」と、「データファイル読み込み関数(空文字列とみなす版)」と、「データファイル読み込み関数(例外を投げる版)」をそれぞれ作る必要がある。

Common Lispだと、ひとつ書けばOKだ。

restart-case、handler-bindという機能がそれにあたるので、このキーワードで調べれば、詳しいことがわかる。

CLOS、総称関数、メソッド

Common Lispのメソッドは、C++JavaRubyPythonなどのようにクラスの中で定義されない。引数に「特定子」というものをつけて、引数の型で呼び出し先が変わる関数(総称関数)を定義する。
C++的に言うと、virtualなfriend関数というのに似ている。(実際にはそんなものはない。)

多態、オーバーロード、ダックタイピング

利点は、「関数オーバーロード」と「ダックタイピング」の両方の利点を同時に得られることだ。

オーバーロードとは、引数の型によって呼び出し先を変えられるということだ。
C++Javaでは、obj.method(a, b)のobjの動的型に従って呼び出し先が決まる。a, bの型によっても呼び出し先を変える(オーバーロードする)ことができるが、これは静的型に従って決まる。
Common Lispの総称関数は、多態+オーバーロードよりもさらに柔軟だ。総称関数は、(method obj a b)という形式で呼び出す。methodの呼び出される実体は、obj、a、b、すべての動的型のパターンによって決まる。
RubyPythonでは静的型がないため、オーバーロード自体をあきらめている。従って、同じことをするには、メソッド内にいちいち型判別のswitch文(相当の何か)を書くことになる。その代わり、静的型についてコンパイラに怒られるわずらわしさから解放される。
Common Lispでは、switch文を書かずに済み、なおかつ厳しい静的型チェックのために無駄なコードを書かされる心配がない。従って両方の利点を満足している。

ダイヤモンド継承問題の解決

また、忌み嫌われる「ダイヤモンド継承」についても、解決法が講じられている。ダイヤモンド継承をしたときのメンバ変数の扱いは、RubyPythonC++のvirtual継承と同じで、「同じ名前のメンバ変数なら同じもの」だ。メンバ(?)関数の扱いに関しては、Pythonと同様に継承関係によって一定の規則(幅優先探索)によって優先順位が決定され、その順番に呼び出されるため、あいまいにならない。
ダイヤモンド継承は、上記のような単純な解決策で大体OKであるにもかかわらず、C++でこれが大問題になったのは、virtual継承とそうでない継承の二つを選択する必要があり、それが非常に理解しづらかったからだと個人的には思う。(オブジェクト指向の学術的な話についてはあまり詳しくないので、的外れかもしれませんが、、、)

まとめ

Common Lispの利点としてはマクロ機能が最大のものだが、それ以外にも注目すべきところはたくさんある。注目されないのはもったいないと感じた。いろんな機能が相俟って、コードをシンプルに短く書けるよう工夫されており、Javaで書くよりも生産性や保守性が劇的に向上することが期待される。

また、Practical Common Lispを読んで、Common Lispが商用品質のコードを書くための機能を十分に備えていると感じた。コンディションシステムひとつを取っても、Javaで書くよりも確実に柔軟なエラー処理を行うことができる。もちろん、GCが許容できない領域(本物のリアルタイムシステムなど)には使えないが、商用のコンパイラや、オープンソースのSBCLでは十分高速に動作するため、JavaVMが原理的にカバーできる適用領域は(ライブラリさえあれば)すべてカバーできると考えられる。

惜しいところ

Practical Common Lispの説明の中で言及されていなかったのは、プロファイリング機能やメモリ使用状況の取得機能などだ。趣味の世界を脱け出して、商用で本当に「実践」するためには、そういう機能は必須で、Allegro Common LispやSBCLなどを少し触ってみる限りでは、十分な機能が備わっているように思われる。(取得値がどのくらい信用できるものかどうかについては未評価)

処理系によって使い方がだいぶ異なるので、説明しづらいのは理解できるが、やはり、性能や品質を分析する観点での情報があれば、最高だった。