2011-11-20

"初めての人のためのLISP[増補改訂版]" 竹内郁雄 著

先月(2011年10月)スティーブ・ジョブズが亡くなって、世間の話題をさらった感があるが、同月に言語界の巨匠が亡くなって、それなりに業界を賑わしている。C言語の開発者デニス・リッチーと、Lispの考案者ジョン・マッカーシーである。カーニハンとリッチー共著の「THE C PROGRAMMING LANGUAGE」(原書)が、本棚に懐かしく並ぶ。
さてLispだが、ずーっと前から触れてみたいと考え、ようやく今年になって少し触れてみた。ポール・グレアム氏の著書「ハッカーと画家」の影響である。そこには、Lispを学べば実際にLispプログラムを作ることがなくても良いプログラマになれると熱く語られていた。だが、いまいち踏み切れない。いかんせん他のプログラミング言語と見た目が違う。S式とかいうヘンテコな宣教師が「カッコカッコ...コッカコッカ」と呪文を唱えれば、カッコの奥からletとかいう控えめな教職者がlambdaとかいう名もない浮浪者に見返り(返り値)をねだってやがる。鬱陶しい奴らだ。今日ではプログラミング言語も多様化しているし、わざわざこの古い言語に立ち返らなくても...という意識がどこかにある。プログラマでもないし。
そんなある日、本屋を散歩していると、一つの宣伝文句に目が留まる。
「道はnilを生ず。nilはアトムを生じ、アトムはS式を生じ、S式は万物を生ず。」
これは、老子の言葉「道は一を生ず。一は二を生じ、二は三を生じ、三は万物を生ず。」をもじっている。nil(ニル)とは、煮ても焼いても食えないやつだ。それは一般的なプログラミング言語のnullに相当し、空っぽを意味する。哲学風に言えば、無形化したもの、空虚なもの、実体を失ったもの、とでも言ってやるか。つまり、無から実が生じると言っているわけだ。孫子の兵法の基本原理「虚実の理」を匂わせれば、泥酔者はイチコロよ!

人間社会が仮想化へ邁進すれば、人間の価値観までも実体から乖離していく。ものづくりの世界では、あらゆるハードウェアがソフトウェア化し、いまや何に価値が生じるかも想像できない。そして、自己の実存にすら確信が持てなくなり、何にすがって生きればいいのかも難しくなる。高度な利便性と溢れる情報量が人々を惑わせ、無形化した何かに恐怖する羽目になろうとは。精神は実体である肉体から離れていく。まるで現実逃避するかのように。いや、仮想化社会だからこそ、逆に精神の内に見出す実体的な何かが必要なのかもしれん。実際、周囲の若い技術者たちには哲学的な観点からシステム思想を考える人が増えてきた。その一方で、システム思想に適合しない流用資産を持ち込んで、開発期間が短縮できるなどという夢想を押し付けるオヤジたちにうんざりする。まさにLispは、データの本質とは何か?とデータ構造の観点から哲学的実体を問うているような気がする。

かつて、プログラマはコードの書き方にエレガントさを感じ、そこに生き甲斐を求めてきた。今日では、データ構造のエレガントさがコード効率を高める。おいらは、データ型の宣言が厳格なものを好む傾向がある。昔は、適度に厳格な仕様がなかったからだ。近年の動的言語では、型宣言がそこそこ緩やかで、ダックタイピング的な特徴があるのはありがたい。
こうして振り返ってみると、無意識にデータとコードを区別して思考している。例えば、関数の構文では、関数本体と引数を区別している。むかーし、メモリ管理方式にセグメントという概念があり、コードセグメント、データセグメント、スタックセグメントで分けていた。おまけにエクストラセグメントなんてものもあったけか。こうした時代に生きた人間には、コードとデータを分けて思考する慣習がどこかに残っているかもしれない。
しかし、だ。Lispは、もっと古い時代から存在するにもかかわらず、プログラムとデータを区別しない。Program as data !これは、いかに実装されるかを想像してみれば当たり前のことだけど。プログラムだろうがデータだろうが、メモリに格納された状態は「アドレス + データ」という単純な構造をしている。関数をcallしたところで、関数の実体が存在するポインタを参照するだけのこと。関数の中身の逐次処理は、順序付きのデータコンテナで構成されたリスト型データと捉えることができる。引数では、値そのものを格納するか、値の存在するポインタを格納するかの違いがあっても、同じくリスト型で抽象化できる。ちょっと視点を変えて、関数が返す値はデータなのだから、関数自体をデータの一種と捉えても不都合はない。データにしても、データそのものを返すquote関数の結果と捉えれば同じく抽象化できる。
そして、リスト型データを配列風にカッコで表現すれば、関数も引数と同列にカッコの中に含まれ、データの多重構造は必然的にカッコの多重記述になる。なるほど、S式はLispポインタに他ならないというわけか。カッコカッコ...コッカコッカという呪文そのものが、リスト型データの合理性を表しているということか。Lispはよく関数型言語と言われるが、その特徴はリスト型データ構造の方がはるかに本質的なのかもしれない。
...と結論づけるのは浅はかで、別の書籍を読むと、実は命令型プログラムの裏返しとして重要な意味があるらしい。それは次回扱うとして、今宵はリスト型を味わうことに主眼を置くとしよう。理屈が見えてくれば、呪文への抵抗感が薄れていく。ありがたや!ありがたや!

本書は、プログラムとデータが相互に立場を変え得ることが、フォン・ノイマン型アーキテクチャの本質だと主張する。その思考は機械語的ですらある。なるほど、Lispはハードウェアと相性が良さそうだ。実際に、機械語でどのように振る舞うか想像しながらプログラムを書く人も少なくなかろう。特に、組み込み系ではそういう傾向が強い。
余談だが、電子回路設計では、ソフトウェア化が進みハードウェア記述言語(HDL)を用いる。そして、更なるシステム設計の効率化を進め、C言語系と連携した上流設計を目指す。ただ、昔からハード屋さんがソフトウェア化を叫べば、やたらとC言語化するものだと考えがちだ。特にC++系を崇める風潮がある。動作合成ツールでは、ひたすらC言語系を用いて動作レベルを記述しようと躍起になる。おまけに、回路の知識がまったくない人に、"Hello world !"プログラムが書ければ回路設計ができると勘違いさせて、現場を混乱させる。
その一方で、システム業界では言語の多様化が進み、C言語系にこだわる事例が減っている。そもそもC言語とHDLの相性がいいのかも疑問だ。Lispの方がはるかにHDLと相性がいいのではないか、と思う今日この頃であった。

1. 動的束縛の言語
「Lispはプログラムの実行時まで呼び出すべき変数のデータ型や関数の実体が決まらない動的束縛の言語である。」
動的束縛は、解釈実行型かつ program as data であることとほぼ同義だという。コンパイラでは速度性能の観点から、プログラム形式に静的な宣言を取り込もうとする傾向がある。そうなると、Lispのような動的言語は時代遅れとされるかもしれない。だが、ハードウェア性能が向上すると、その場で解釈しながら実行できる言語がもてはやされる。そういえば、インタプリタという用語をとんと聞かなくなった。実行ニュアンスがちょっと違うかなぁ。
また、ソフトウェアが大規模化すれば、トップダウンで要求仕様をしっかり決めないとプログラムが書けないという風潮がある。しかし、ボトムアップでプロトタイプをすぐに書けるというのは大きな魅力だ。トップダウンで構成された大規模なプログラムを、アップデートすることは面倒である。近年、システムを停止しないままアップデートすることが求められ、プログラムを動的に管理する必要がある。
ところで、動的といえば、Lisp言語の歴史そのものがまさに動的であるという。ようやく、Common Lisp と Scheme の二大方言に落ち着きつつあるようだが、永久に動的であり続けるのかもしれない。ちなみに、ガベージ・コレクションを最初に実装したのが、Lisp1.5 だそうな。現在では、Java, Python, Rubyなど多くの言語システムで実装されている。いわばゴミ集めの機能だが、これを言語システムで持つ必要があるのは、動的言語の宿命であろうか。

2. S式とM式
言語の最大の目的は、意味論的な機能を果たすことであろう。自然言語の優位性は、人間の感情を取り込むところにある。一方、コンピュータでは、感情を排除し一義的に処理する能力が求められる。しかも、コンパクトに記述するとなれば、そのほとんどの機能は数学的な表現となろう。
初期のプログラムは、関数表記に近いM式(Meta Expression)で書くものとされたらしい。M式とは、S式(Symbolic Expression)を引数として扱う表記で、例えばこんな感じ。

 length[x] = [null[x] → 0; t → add1[length[cdr[x]]]]

んー、見る気がしない!
もともとS式は、データやプログラムの内部表現を書くための低レベルの表記法だったという。例えばバイナリダンプのような。やがてM式は廃れるが、カッコだらけのS式が一般受けしなかったのは言うまでもない。
「Lisp嫌いは、S式をLispのよい特徴を覆い隠す必要悪と考えているらしい。」
S式は前置記法によく適合するだけでなく、言語意味論を簡単に乗せることができるという。それは二つのデータ、すなわちリストとアトムで構成される。リストはデータのペアを表わし、アトムはその素材である数字や文字列を表す。アトムはこれ以上分解できない物質の原子のようなもので、リストはアトムのペアで構成される分子のようなものというわけ。リストの表現は、アトムを羅列してカッコでくくったもので、コンマなどの区切りはない。リストの基本は二進木構造にあり、並べられたアトムはその順番に意味を持つ。そして、S式だけが yes か no を答える。もっとも、yesは T(true)、noは nil となる。言語学的には、必要最小限の情報量を与えているということはできそうだ。人間の判断力の根幹とは、まさに素材と選択肢、つまりはアトムとリストにあるのではないか!ちと大袈裟か。

3. ちょいとリスト型を味わう!
Lispの発明者マッカーシー先生は、リストの一番目の要素を、car と呼んだそうな。そして、1個以上の要素を持つリストから car を除いた残りのリストを cdr と呼んだそうな。
carとは、Contents of the Address part of Register number の略、
cdrとは、Contents of the Decrement part of Register number の略。そんなウンチクはどうでもええ。ちなみに、car を head 、cdr を tail と呼ぶ人もいるそうな。
それでは、ちょうど目の前に、GNU CLISP 2.48があるので遊んでみよう!
car関数を評価すると...

 (car (goo choky pah))
 *** - EVAL: undefined function GOO
 The following restarts are available:
 USE-VALUE :R1 Input a value to be used instead of (FDEFINITION 'GOO).
 RETRY :R2 Retry
 STORE-VALUE :R3 Input a new value for (FDEFINITION 'GOO).
 ABORT :R4 Abort main loop

goo が評価できる関数ならば答えが返ってくるのだろうが、そうは問屋が卸さない。関数の引数が関数であってもいいのだ。
 (car '(goo choky pah))
と引用符を付けると、リスト(goo choky pah) に対するcar が評価されて、gooが返ってくる。んー、引用符の使い方に意義があるのかぁ。
引用符を用いるのは記法上の便法で、Lispがデータの皮を食ってしまうという。なんじゃそりゃ!
 (+ 1 2 3 4 5) は、 (+ '1 '2 '3 '4 '5) と書いても同じ。
 (car '(a b c)) を評価すると、aを返す。
 (cdr '(a b c)) を評価すると、(b c)を返す。
リストを作る時は、cons関数を使う。
 (cons 'x '(y z)) を評価すると、(x y z)を返す。cons関数は carや cdr の逆関数と解釈できそうだ。
アトムの判定には、atom関数を使う。
 (atom '(x y)) を評価すると、nilを返す。アトムではなくリストというわけか。
 (atom nil) を評価すると、Tを返す。nilはアトムというわけか。
また、辞書の語彙を増やすのに、set関数がある。
 (set 'A 'aho)
A と aho に引用符を付けて関数の引数と評価している。
よく見かけるのに、setq(set quote)という関数もどきがある。
 (setq A 'aho)
Aに引用符を付けずに関数の引数と評価させて、自動的にうまいことやっているわけか。尚、このような評価を特別扱いするものを、「特殊形式」と呼ぶらしい。
関数が数値や文字と同じようにアトムとして扱われるだけに、その評価も統一性がある。ただ、直観的に左側のアトムの評価が鍵を握っている。更に、第一引数に特殊な意味を持つ場合があることにも注意したい。感覚的には、他の言語では文法に注意するが、Lispではアトムの評価の順番に注意すればよさそうだ。
ちなみに、CLISPを終了する時も(exit) とカッコを付けないと終了しない。(quit)も同じ。
 (atom 'exit) を評価すると、Tを返す。コマンドもアトムというわけか。
これだけでもリスト型言語であることが味わえる。

4. 再帰的思考に威力あり!
ここで、リストの長さ(アトムの数)を求める関数を作ってみよう。

 (defun lengthc (x)
   (cond ((null x) 0)
     (t (1+ (lengthc (cdr x)))) ))

 (lengthc '(aa bb cc)) を評価すると、3を返す。

リストの長さは、空リストなら0、そうでなければcdrの長さに1を足していく。これが、Lisp風の再帰的思考だそうな。
これを通常のプログラム風に思考すると、次のようになる。
(1) カウンタ値をリセット。
(2) リストが空ならば、そのままカウンタ値を返す。
(3) xを元のx のcdrに置き換えてカウンタに1を足す。そして、(2)に戻る(loop)。
これをLispでも書けなくはない。

 (defun lengthd (x)
   (prog (count)
        (setq count 0)
  loop  (cond ((null x) (return count)))
        (setq x (cdr x))
        (setq count (1+ count))
        (go loop) ))

尚、Lispには、progという特殊形式が用意されていて、(prog(x y z)...)で、ローカル変数を宣言できる。loopから抜ける時の値は、returnで返す。
更に、do文を使えば軽快になる。

 (defun lengthe (x)
   (do ((count 0 (1+ count)))
     ((null x) count)
     (setq x (cdr x)) ))

こうして比較してみると、Lispは再帰的思考に威力を発揮することが見えてくる。しかし、昔から計算機的な思考は再帰法なんか使わずに、BasicやFortran風に goto, do, loop が主流であった。Lispの基本的な思考は再帰的であるために数学の匂いを強く感じさせ、これが敬遠される理由だったという。それだけではないだろうけど。再帰が深まるとリストの長さに比例したメモリを消費するが、従来のプログラム思考では、変数count だけの領域でいい。かつてはハードウェア資源に制約がある場合がほとんどだった。物理的な制約からも、再帰的な思考は避けられていた。そういえば、オブジェクト指向という言葉が登場したあたりからであろうか。gotoプログラム撲滅運動が始まった。単純な繰り返しならば、loopラベルで明記できるが、ラベルが乱立すれば、たちまちスパゲッティ状態になる。検証も思いっきりやりずらく、ブランチカバレッジを真面目に見ようものなら頭が痛い。ただ、人間の一般的な思考方法において、再帰的思考は馴染まないかもしれない。再帰だろうがloopだろうが、精神ってやつは自己循環論に陥りやすく、おまけに自己矛盾に悩みながら精神病を患うのは同じか。

5. lisp の核はコンパクト!
  1. nil、数、文字列は、評価するとそれ自身を返す。
  2. シンボルの評価値はその時点での環境によって決まる。
  3. シンボルの評価値は、第0要素を関数、第1要素以下を引数と見なし、左から順に評価する。ただし、次の、4. 5. を除いて。ちなみに、関数を引数リストに適用(apply)すると言うらしい。まさしく、apply関数はこれをやっている。
  4. リストの第0要素が次のものであった場合、第1要素以降をどう処理するかは、第0要素により異なる。quote, cond, setq, prog, progn, go, let, let*, if, do, do*, defun, defmacro, functionなどは、第1要素以降を闇雲に評価せず、引数の評価を各自がきめ細かくコントロールする。例えば、quoteは第1要素を評価しないでそのまま返す。
  5. リストの第0要素がマクロであった場合、第1要素以下を評価せずにマクロの本体に渡す。そして、マクロ本体が返してきた値をその場でもう一度評価する。
実は、4.の中の特殊形式にも、Common Lispではマクロとされるものがあるそうな。例えば、condやdoなど。

6. ちょいと気になるスコープルール
動的スコープが用意されているのにちょっと警戒する。これは言語仕様の根本的な問題となろう。動的スコープを野放しにすれば、他の言語派から迫害を受けるに違いない。そこで、しかるべき非局所的な変数に限るという意図があるという。letやdoは変数を束縛する機能を持っている。
確かに、システムの中で常識的な変数をグローバルに参照できる方が、効率が良い場合がある。変数を参照するだけのメッセージ関数を用意するのも面倒だ。
動的スコープの変数は、special変数と呼ばれ、非局所的に宣言して、*で囲む慣習があるそうな。例えば、*standard-input* といったもの。最初から動的スコープと分かっているやつは、宣言する必要もないらしい。静的スコープの良さは、分かりやすさを求めるというより、ミスを防ぐことを重点に置く。ミスを回避して分かりやすくなるのであれば、動的スコープもありか。実は名前空間が適度というのは非常にありがたい。ただ、適度というのがくせものだけど。いずれにせよ、自由が野放しにされるのは危険であろう。その意味で大人の言語と言えるかもしれない。
Common Lisp では、名前空間をシンボル・パッケージ、あるいは単にパッケージと呼ぶそうな。標準装備されているパッケージは、lisp, user, keyword, sysytem の4つだという。lisp にはcarやcondなど一般的に使われるシンボルが、user にはユーザ定義のシンボルが入っている。system にはLispの言語処理系が内部で使用するだけで一般公開されないシンボルが入っている。ほんで、残りがkeyword。
lisp:car に対して、ユーザが、user:car や keyword:car を作ってもいい。文脈がはっきりしていれば、パッケージ名は省略できる。ただし、keywordだけは特別扱いされ、例えば、keyword:direction は、:direction と書けるそうな。

7. prog と let
progは、4つの基本機能を統合した特殊形式だという。、
(1) ローカル変数を宣言する。
(2) goによって繰り返しや飛び越しを可能にする。
(3) 計算の途中でreturnで強制的に値を返す。
(4) 不定個の式を上から順に評価する。

(4)の機能は、progn という形式も用意されている。
 (progn 式1 式2 式3)
ただ、頻繁に複数式が用いられるので、progと書いてもprognと解釈されるのが一般的だという。
(4) だけなら、progn と書けば複数式が強調される。
(1) と(4) だけなら、letを使うとよい。変数宣言と式の逐次評価機能だけに的を絞ったのがletで、return不要で気分爽快!

8. Lispの得意な分野
Lispは、データ構造が配列や連想リストなどで構成されている場合に威力を発揮するであろう。すぐさま思い浮かべるのは、行列式などの数値演算や、画像処理などの空間処理を扱うような分野である。本書は、自然言語やプログラミング言語の言語処理、あるいはデータベースのような分野でも威力を発揮するとしている。
ちなみに、配列では、setqよりも抽象化されたsetfを使うのがよいという。Common Lispで推奨されるのは、setfだそうな。配列要素へのアクセスには、aref(array reference)が用意されている。
本書は、データ構造が伸び縮みする例題として、キューとスタックの実装を取り上げている。キュー(待ち行列)はFIFO構造、スタックはLIFO構造をしている。キューの制御例では、DeQ/EnQ、スタックの制御例では、Push/Popがお馴染みで、通信系の回路設計に携わると必ず登場する技術だ。
また、ツリー構造の例題として、Jpegなどの圧縮フォーマットで用いられるハフマン符号化を取り上げている。そういえば、むかーしFAX系の通信システムでハフマン符号の回路設計にも携わった。
...などと、Lispを初めて使うはずなのに、なにやら懐かしいものを感じる。これがデジャヴってやつか?

0 コメント:

コメントを投稿