著作一覧 |
LET OVER LAMBDAが届いたので読み始めたらめっぽうおもしろい。
著者は、『はじめに』の時点から突っ走る。
知性を持つプログラマが、いったんプログラミング行為を論理的手続きと考え始めれば、その手続きの論理的な次のステップは、自動化そのもので利益を享受することである。結局、プログラマはまさにこの自動化工程を遂行すべく訓練される。
そのためにはLispだしマクロだと主張する。というのは、
メタプログラミングは、すべてのプログラミング言語で多かれ少なかれ採用されている。だが、Lispほどそれを徹底して取り入れた言語は他にない。他のどんな言語も、メタプログラミングテクニックに都合の良いコーディングをプログラマに要求したりはしない。だからこそ、非LispプログラマからはLispプログラムが奇天烈に見えてしまう。Lispコードがどう表現されるかは、Lispのメタプログラミングの要求から来る直接の帰結なのである。Lispコードそれ自体にある。
この本は、Lispのマクロの本だが、「はじめに」で大上段に構えているように、プログラムコードをプログラムすること、つまり自動化、メタプログラミング、言語内DSLの考え方の本としても読める。その点と、その薄さ(本文だけなら300ページ弱)から、実のところ、この本を本当に読むべきは、一番層が厚いわりには中級者あたりのところで止まっている人が多そうな言語、つまりJavaに熟練したプログラマとかではないかな、とも思える。
が、もしLispのことを(カッコ 多い (奇妙p 言語)))くらいしか、知らなければ、そもそも読みようがない。それに、たぶん、その後もLispを使い続けることもないのではなかろうか。とすれば、この本を読むために、Lispの入門書を頭から読んだりするのは時間が無駄だろう。
記法については、適当に探せばいくらでも見つかるわけだから、Lispのコードとはどういうもので、なぜそれが「メタプログラミングテクニックに都合の良いコーディング」なのかを押さえておけば良いのではないだろうか。
というわけで、Lispがどういう言語かについて解説する。
LispはList Processingという名前の通り、リストを操作するための言語で、プログラム自体がリストで構成される。
リストとは、要素の繋がりのことだ。
その要素を格納するために、コンスセルを利用する。Javaで実装するとすれば、以下のようになる。
public class ConsCell { Object car; Object cdr; public Object getCar() { return car; } public void setCar(Object o) { car = o; } public Object getCdr() { return cdr; } public void setCdr(Object o) { cdr = o; } public boolean isNil() { return false; } public boolean isList() { return true; } }今、3要素、0,1,2を持つリストを作るとする。すると、以下のようになる。
ConsCell elem1 = new ConsCell(); ConsCell elem2 = new ConsCell(); ConsCell elem3 = new ConsCell(); elem1.setCar(1); // auto boxing elem1.setCdr(elem2); elem2.setCar(2); elem2.setCdr(elem3); elem3.setCar(3); // リストをプリントする System.out.println("("); for (ConsCell c = elem1; c != null; ) { System.out.println(c.getCar()) c = c.getCdr(); if (c != null) { System.out.println(" "); } } System.out.println(")");
各コンンスセルは、cdrフィールドで連結される。最後のコンスセルのcdrはnullなのでそこがリストの終端となる。
このように、リストは、0個以上のコンスセルから構成される。ここで、0個以上というのは重要で、特に0個のリスト(空リスト)をNILと呼ぶ。
リストを構成するコンスセルに対して、それ以外をアトム(原子のこと。それそのものでしかない)と呼ぶ。上の例では、1とか2とか3はアトムだ。文字列もアトムだ。
そして、重要なアトムがある。それはNILだ。
NILは空リストなので当然リストだ。しかし、コンスセルではない(0個のコンスセルと言っても良い)。したがって、アトムでもある。
もっとも、本当に空でアクセスすると例外となるJavaのnullとは異なる。NullObjectパターンの実装を考えると、より近いものとなる。
public class NIL extends ConsCell { private NIL() {} public final static NIL NIL = new NIL(); public Object getCar() { return this; } public void setCar(Object o) { ; } public Object getCdr() { return this; } public void setCdr(Object o) { ; } public boolean isNil() { return true; } public boolean isNil() { return true; } public boolean isList() { return true; } }NILクラスの導入により、元のConsCellクラスの実装は以下となる。
public class ConsCell { Object car = NIL.NIL; Object cdr = NIL.NIL; public Object getCar() { return car; } public void setCar(Object o) { car = o; } public Object getCdr() { return cdr; } public void setCdr(Object o) { cdr = o; } public boolean isNil() { return false; } public boolean isList() { return true; } }
Lispのプログラムは、これらのオブジェクトを利用して記述する。
たとえば、以下のプログラムは1と2を加えた結果を返す(Lispは最後に評価した結果を返す)。
(+ 1 2) * 3 ←結果
このプログラムは、概念的には(実際には機械語などにコンパイルされるからだが)、以下のようにコンパイルされる。
top = new ConsCell(); ConsCell elem2 = new ConsCell(); ConsCell elem3 = new ConsCell(); top.setCar(+); top.setCdr(elem2); elem2.setCar(1); elem2.setCdr(elem3); elem3.setCar(3); Method m = top.getCar().getMethod("default"); return m.invoke(elem2);
すべてのリストは先頭要素で指定した関数に対して、2番目以降の要素を引数とした関数呼び出しとなる(マクロに展開されることもある)。関数を呼び出して結果で置き換えることを「評価」と呼ぶ。
ただし、先頭に「'」(クォート)を付けてクォートすると評価をスキップできる。
'(+ 1 2) * '(+ 1 2)
なぜ、この仕組みが「メタプログラミングテクニックに都合の良いコーディング」なのだろうか。
それは、プログラムが最初からリスト(というデータ構造)で示されているからだ。
ほとんどのプログラミング言語は、テキストとして記述されたプログラムを処理系が内部で処理する。かりにソースをプログラムから操作したい場合、それはテキスト処理となる。
それに対してLispではソースをプログラムから操作したい場合、それはリスト処理となる。そして、Lispは元々リスト処理に特化した言語である。したがって、メタプログラミングテクニックに都合が良いのは当然である。
LET OVER LAMBDA Edition 1.0(ダグ ホイト)ジェズイットを見習え |