2014-07-13

"実践 Common Lisp" Peter Seibel 著

本書を購入したのは、三年ぐらい前になろうか。時々横目で追い... 辞書代りにし... これを読破するには気が重い。ところが、改めて目を通してみると、意外とストーリー性があって、おもろいんでないかい!
それは、音楽ファイル(MP3)からタグ情報(ID3)を抽出し、曲情報をweb上で管理するという物語で、最終的に、曲の検索、プレイリストの追加、ストリーミングといった機能を備えるMP3ブラウザを作成することにある。
ここに含まれる技術要素は... 単語の頻度を調べるために、スパムフィルタでよく見かけるベイジアンフィルタを用いて学習する仕掛け。バイナリファイルのためのパーサの作成。リスト型言語らしいデータベースの構築。WebアプリケーションのためのWebサーバ(AllegroServe)の導入。HTMLライブラリの生成。ストリーミング再生のための、Shoutcastプロトコルの実装など...
実践と言うには、ちと話題がづれていそうだが、試してみると、なかなか役に立ちそうではないかい。もちろん、Common Lisp の処理系や、CLOS(Common Lisp Object System)といった話題も豊富!満腹すぎて吐きそうなほどに...

Lispは、FORTRANの時代を思い出させる言語で、その歴史は1950年代に遡る。だが、改良を重ね重ね、いまだ輝きを失っていない。オブジェクト指向の機能をもたらす CLOS は、もはや Common Lisp と分けて論じることができないほど馴染んでやがる。
今日のコンピューティングは、メモリ容量やCPU性能、あるいは、表示システムの高精細化や入力システムの操作性向上など、リソースは格段と進化している。プログラミングにおいても、ハードウェアをほとんど意識しなくて済み、本来の目的に傾注できる高水準言語が続々と登場する。おそらく万能な言語は存在しないだろうし、ドメイン固有言語という発想はますます広がるだろう。
そんな時代にあってもなお、Lisp が生き残ってこられたのは、言語自体に柔軟性があるのも確かであろうが、チューリング等価の記法という意味では、ソフトウェアはあまり進化していないのかもしれない。いずれにせよ、チューリングマシンが考案されて一世紀にも満たず、コンピューティングの歴史はいまだ過渡期にある。実際、新しい技術が品質やユーザビリティを落とすケースも珍しくないし、登場時期が早すぎたために廃れていくアイデアも少なくない。この手の書に触れる時は、新しいとか、古いとか、そうした意識は捨ててかかった方がよさそうである。引退間際の古代人には、なんでも真新しく映るけど...

Lisp信者は、こんなことを言う。Lispを学べば、もっとましなプログラマになれると。言語に対する思いは宗教的なところがあり、その座右の銘は禅問答に似たものがある。Lispの場合はこれだ。
「プログラム可能なプログラミング言語」
彼らは、マクロ機能の強力さをこわだかに話す。まるで万能言語であるかのように。だが、手続き型言語に慣れ親しんできた者にとっては、まるで宇宙人の文化だ。S式とかいうヘンテコな宣教師が、カッコカッコ... コッカコッカ... と呪文を唱えれば、カッコの奥から let とかいう控えめな教職者が lambda とかいう名もない浮浪者に見返り(返り値)をねだってやがる。実に、鬱陶しい奴らだ!
しかしながら、異質の言語に触れることにも意義がある。当たり前と思い込んでいる事でも、疑問を持つ機会を与えてくれる。最近、気に入っている感覚は、リスト型データ構造である。Lispが記号処理のために設計され、list processing を得意とすることは、その名が示している。
「リストは不均質で階層的なあらゆるデータを表すための格好のデータ構造だ。」
仕事では数学のアルゴリズムを検討することが多く、関数で多次元空間を記述する場合、返り値には多値を用いたい。例えば、C言語などの return変数は基本的に一つで、多値を返したければポインタ参照の形をとる。これが副作用の原因となりやすい。わざと副作用を利用することもあるので、一概に悪いとは言えないのだが。一方、リスト型データはもともと多値が想定されているので、そんな気遣いはいらない。尚、ここで言う「多値」とは、群論で言うところの集合体を意味し、論理学のそれとは意味が違うので注意されたし...
「関数プログラミングの真髄は、与えられた引数のみに依存して演算を行う副作用を持たない関数のみでプログラムを構成されることにある。」
本書は、リスト構造が柔軟であるがために、その弊害で、ユーザがリストに特化しすぎる傾向があると指摘している。そして、もっと効率的な方法として、コレクションの概念を用いたベクタやハッシュテーブルの事例を紹介してくれる。また、リスト型が複素数に対応してくれるのもありがたい。尚、今日では多くの言語で複素数型がサポートされている。C99ライブラリのように...

1. コレクションの概念
「リストを理解する手がかりは、そのほとんどがより基本的なデータ型のインスタンスであるオブジェクトの上に構築された幻想だと理解することにある。その単純なオブジェクトはコンスセルと呼ばれる値のペアであり、関数 CONS を呼び出すことによって作り出される。」
リスト型データ構造を効率的に用いる方法として、コレクションが紹介されるが、明確な違いがよく分からん!コレクションを操作する方法として、シーケンスというサブタイプがあるが、その多くはリストでも使えそうだし。ただ、シーケンスは非常に強力なために、リスト操作とは抽象度の違いを感じるのも確かで、コレクションという用語を新たに生み出すだけのことはありそうか。

例えば、ベクタでは...
固定サイズに vector関数、固定サイズと可変サイズの両方に make-array関数が用意される。要素の追加と削除には、vector-push, vector-pop があり、シーケンスのレベルでベクタとリストが区別されるかに見える。反復関数には、count, find, position, remove, substitute などがあり、これらの関数の末尾に、-if を付けて条件式として使える。count-if, find-if... といった具合に。
シーケンス全体の操作では、copy-seq, reverse、あるいは、連結に、concatenate がある。ソートには、sort, stable-sort。尚、stable- 述語で順番が入れ替わらないことを保証する。部分シーケンス操作には、subseq。尚、範囲は開始と終了インデックスで指定。他には、marge, search, mismatch など。
また、シーケンス述語に、every, some, notany, notevery がある。
 every    : すべての述語が満たされれば t、それ以外 nil。
 some     : 1つでも満たすものがあれば t、すべて満たさないとき nil。
 notany   : 1つでも満たされれば nil、1つも満たさない場合 t。
 notevery : 1つでも満たされれば t、1つも満たされない場合 nil。

複数のシーケンスに対して関数を施したい時は、map, map-into といったマッピング関数が使える。
また、一つのシーケンスに対しては、reduce が便利。
合計を求めるには...

  (reduce #'+ #(1 2 3 4 5 6 7 8 9 10))  => 55

最大値を見つけるには...

  (setf numbers '(10 12 14 16))
  (reduce #'max numbers)  => 16

キーワード引数には、:key, :from-end, :start, :end がある。
固有の引数には :initial-value があり、シーケンスの初期値が設定できる。

ハッシュテーブルでは...
生成に、make-hash-table。要素にアクセスするには、gethash。gethashに複数の返り値がある場合は、 multiple-value-bind マクロが用意される。
反復処理は、maphash でこんな感じ...

  (maphash #'(lambda (k v) (format t "~a => ~a~%" k v)) *hash*)

2. 例外処理とコンディションシステム
Lispの偉大な機能の一つにコンディションシステムがあるという。Java や Python や C++ における例外処理と似た目的で提供されるが、もっと柔軟でエラー処理にとどまらないという。プログラムの実行中に起こる出来事を記述できるので、例外よりも汎用性が高いということらしい。例外処理においては、エラーの捕捉とその通知が鍵となるが、Lispでは更に再起動という機能が付加される。
ところで、エラー処理とは、なんであろうか?プログラムは関数の階層によって組み立てられる。低位の関数の上に高位の関数が構築されれば、実行中の実体はコールスタックという形で現れる。低位のプロセスは、高位のプロセスのコールスタック上にあるということだ。
したがって、エラーを捕捉する格好の場所は、関数の境界ということになろう。実際、低位と高位の依存関係において問題が発生しやすい。例えば、呼び先のファイルが存在しないとか、メモリの空き容量が足りないとか、ネットワークが落ちているといった原因で。関数単体から見て、想定外の条件への対処とも言えよう。一方で、関数内で起こるエラーはそれこそバグであり、ここで扱う問題ではない。
例外機構を備えていないシステムでは、エラー通知は関数の呼び出し元に送ることになろうか。そして、復帰のための処理をするか、失敗を放置するかは、呼び出し元が決定することになる。単純にスキップするだけで何事も起こらなければいいが、現実はそう甘くない。
しかし、例外機構が具わっていれば、コンディションを監視することによって、何らかの対処をシステムレベルで可能にする。復帰処理において、スタックを巻き戻して関数の再起動を試みたり、保険処理のようなものも定義できそうだ。ただ、あくまでも想定外の条件に対処するわけで、却って仇となることもあろう。
Common Lispでは、エラーを回復するコードと、どうやって回復するかを決めるコードが分離されているという。戦略的には、どうやって回復するかは高位の関数に委ね、回復のためのコードを低位の関数に記述できるという仕掛けか。しかも、コンディションは一般オブジェクトと同等な扱いで定義できるようである。再起動によって効果をもたらすには、明確にコードを呼びださなければなるまい。復帰条件も異なろうが、複数の再起動が定義できるようである。コンディションハンドラは、警告をエラーに昇格させることも、その逆もできそうだ。
通知の基本関数は、signal 。なんとなく馴染みのある名前だ。これだけでプログラムのコンディションというよりは、システムコールレベルを想定していることが分かる。コンディションを理解する鍵は、コンディションを通知しただけでは制御フローに影響しないことを理解することだという。
「エラー処理には、プログラミングの教科書であっさりとしか説明されないという残念な宿命がある。エラー処理が適切かどうかは、解説用のコードと製品レベルの品質コードの最も大きな違いだといえる。後者を書くコツは、特定のプログラミング言語の構文の詳細ではなく、ソフトウェアについて特別な厳しい考え方を身につけることにある。」
ただ、あまり柔軟すぎると例外処理と通常処理の境界が曖昧になりそうだ。最もコードのセンスが問われるところでもあろうか。いずれにせよ、最悪の場合、リセットすべきか?再起動すべきか?あるいは、他の処理をすべきか?それはシステムによっても違ってくるし、ソフトウェアだけの問題ではない。
ちなみに、某原発事故では、あろうことか!電源を供給する電力会社が、電源を失うことを想定していなかったと平然と語られた。多くのシステム屋さんにとって、戒めの言葉に聞こえたことだろう...

3. loopマクロ
「皮肉なことだが、マクロを正確に理解する最大の障壁は、おそらくマクロがあまりにうまく言語に統合されてしまっていることにある。」
Lispを使っていると、まずもって不思議な感覚に見舞われるのは、マクロと関数の違いが曖昧にさせられることである。引数も取れば、値も返すし、ちょっと風変わりな関数にしか見えない。だが、実装は全く違う。マクロは単純に展開されるだけに、ローカル変数の束縛で悩ましい。だが、変数名が重複しないように、with-gensyms が用意される。
関数は言語の構文に従うが、マクロはそんな制約を受けないと言えばそうだが。あるいは、マクロはある種の翻訳機とも言えるわけだが...
「言語を"コアに標準ライブラリを追加したもの"と定義する利点のひとつに、理解や実装が容易になることがある。しかし本当のメリットは、言語が容易に拡張できる表現力にある。なにせ、言語だと思っているものの大半はただのライブラリなのだ。」
この感覚を味わうには、loopマクロがうってつけである。ループ構文には、do, dolist, dotimes とったものがある。ただ、これらの表現力は大げさ過ぎる。Lispで書かれていながら、using などの副節でカッコが省略されるところは、実にLispらしくない。
「なぜ LOOP の作者がこの副節で括弧なしスタイルに怖気づいたのか、私に訊かないでほしい。」

loopマクロの主な機能部品は、ざっとこんな感じ...
  • ローカル変数の生成とループ変数の自動更新。
  • 値の収集(collect)、計数(count)、合計(sum)、最小化(minimaize)、最大化(maxmize)。
  • 任意のLisp式の実行。
  • 条件付き終了。
などなど...
おまけに、前処理(initially)と後処理(finally)の節を用いて、ループの前後に任意の処理が指定できる。このあたりの実装は、unix上で動く awk の思想を感じる。

4. format関数
「Common Lisp の FORMAT関数は、LOOPマクロと並んで人々を感情的にさせる機能だ。信者もいればアンチもいる。」
format関数の複雑な制御文字は、電話のノイズに似ているという。確かに、printf風でありながらまったく違うし、正規表現とも違う。短く書けることが最善とするならば、あらゆる文書は暗号文となろう。いや、呪文か!
ただ、暗号ってやつは、解き明かせば恐れるに足らん。まず、すべての指示子は、~(チルダ)で始まる。指示子によっては、前置パラメータをとる場合がある。前置パラメータは、チルダのすぐ後に書き、複数ある場合はコンマで区切る。最も汎用な指示子は、~a で、人間の読める形に出力する。これだけ押さえれば、大概のことは解読できそうである。
改行を出力するには、~%。新しい行を出力するには、~&。尚、~% は常に改行するのに対して、~&は行頭でないときのみ改行する。

  (format t "~a~%" list)
  (format t "~5$" pi)  => 3.14159  ($ は小数点表示、デフォルトは2桁)
  (format t "~d" 1000000)  => 1000000 (d は10進数表示、他に、~x, ~o, ~b がある)
  (format t "~@d" 1000000) => +1000000 (符号付き)
  (format t "~:d" 1000000) => 1,000,000 (3桁ごとに区切る)
  (format t "$~:d" 1000000) => $1,000,000 (通貨単位ドル)

条件による整形には...
~[, ~] で囲んで、条件分岐を指示する。

  (format t "~[cero~;uno~;dos~]" 1)  => uno (~; で区切ってインデックスで指定)

~{, ~} で囲んで、反復を指示する。

  (format t "~{~a, ~}" (list 1 2 3)) => 1, 2, 3,

最も驚かされるのは英文制御が凝っていることだ。使うかどうかは別にして...
~r は、英語の指示子。

  (format t "~r" 1234)  => one thousand, two hundred and thirty-four
  (format t "~@r" 1234)  => MCCXXXIV (@ はローマ数字)
  (format t "~:@r" 1234)  => MCCXXXIIII (:@ は古いローマ数字)

単数形と複数形の制御に、~p を用いると、複数形の時、sを付加する。

  (format t "~r file~:p" 1)  => one file
  (format t "~r file~:p" 10)  => ten files

さらに、@ は単数形の時 y、複数形の時 ies を付加する。

  (format t "~r famil~:@p" 1)  => one family
  (format t "~r famil~:@p" 10)  => ten families

大文字と小文字制御では、~(, ~) で囲んで、その間の制御文字列を小文字で出力する。
@ は文字列の最初の文字が大文字。: はすべての単語の頭が大文字。両方つけると、すべて大文字。

  (format t "~(~a~)" "THE QUICK BROWN FOX")  => the quick brown fox
  (format t "~@(~a~)" "THE QUICK BROWN FOX")  => The quick brown fox
  (format t "~:(~a~)" "THE QUICK BROWN FOX")  => The Quick Brown Fox
  (format t "~:@(~a~)" "THE QUICK BROWN FOX") => THE QUICK BROWN FOX

5. Lisp in a BOX
REPL(read-eval-print loop)とは、読み取り(read)、評価(evaluate)、印字(print)の終りのないサイクル、すなわち、対話式機構のこと。この環境を手っ取り早く提供してくれるものに、「Lisp in a BOX」というものがあるそうな。それは、Emacsと Common Lisp開発環境 SLIME をパッケージ化したものだという。elispにはかなり方言があるようで、やはり SLIME がよさそうである。Emacs風のIDEといったところか...

6. AllegroServe
最近のブラウザは余計な機能が多すぎる。核となる機能は、Webサーバからページをリクエストし、それをレンダリングすること、これだけでもかなり遊べる。
仕事では、ドキュメント関連を、XMLで書け!って要求されることがある。閲覧するだけなら pdf でもよかろうが、検索機能や入力フォームなどが欲しいというわけだ。そのくせ向こうからは、word や excel の文書が提供されるけど。この手の自動生成ツールは、スペックが大げさで、しかもろくなコードを吐かない。結局、機能を限定したXML生成ライブラリを書く羽目に...
そこで、コードのテスト用に、簡易的なサーバが立ち上げられるとありがたい。本書は、そんな時にうってつけのWebサーバを紹介してくれる。AllegroServe と PortableAllegroServe だ。Rubyで言うところの、WEBrick のようなものか。ちなみに、Hunchentoot ってのもよさそう。

パッケージを導入するには...
Allegro Common LIsp ならば、AllegroServe に対して、require する。

  (require :aserve)

他のLispシステムでは、PortableAllegroServe に対して、require の代りに loadする。

  (load "./portableaserve/INSTALL.lisp")

localhostサーバを定義するには...(例えば、ポート2001の時)

  (net.aserve:start :port 2001)

ファイルやディレクトリを公開するには...

  (publish-file :path "/hello.html" :file "/tmp/html/hello.html")
  (publish-directory :prefix "/" :destination "/tmp/html/")

後はブラウザで、http://localhost:2001/hello.html にアクセスすればいい。

0 コメント:

コメントを投稿