2010年5月21日金曜日

!Lisp Ver.1.0

いや、最初は色々と




「LispによるLispの実装?んな、言語設計者になりたいわけでもねえのにメンド臭い。っつーかさ。それってハッキリ言うとComputer Scienceの極めてTrivialなネタなんじゃねえの?やってられっか。Computer Science学科に在籍した事もねえのに。バカヤロー。」


的な文句言ってたわけなんですが。気づいたらすっかりハマってました(爆)。いや、意外と面白いんだ、これが(笑)。



今回は2回目のLisp実装への挑戦、って事で日曜日辺りからやりはじめたのかな?今回参考にした書籍は対話によるCommon Lisp入門 POD版という本。最近、#9LISP用にLOLにかかりっきりで、それはそれで楽しいんですが、週末時間が足りなくってストレスが溜まってた。そこで気分転換にとか思ったんですが、色んな意味でドツボにハマってました(爆)。



前回のSmall-Lispの実装を通じて、


  • クロージャの実装ってどうやんの?


とか言う疑問があって。その点対話によるCommon Lisp入門 POD版では曲りなりにもCommon Lispを実装しよう、と言うネタなんで。ああ、これは前回のSmall-Lispの実装をまた一段上に上げられるかな?とか楽観的に考えてたんですが。目論見が大失敗(爆)。完全に別の実装として考えないと整合性が持てない、という結論に到達致しました。だからまたVer.1.0なんですよね。



まあ、その失敗の理由も色々とあるわけですけれども。そもそも対話によるCommon Lisp入門 POD版と言う書籍名が示すように、これはCommon LispでCommon Lispを実装しましょう、ってネタなわけですよ。それをSchemeでやろうと言うのがそもそも大間違いだった(爆)。んで色んなトコでハマるハマる(笑)。



まあ、後で細かいトコの感想は列挙していこうとは思うんですけど。そもそも対話によるCommon Lisp入門 POD版では、CLOS(Common Lisp Object System)使ってCommon Lispの基本的な機能のみのサブセットを作りましょう、ってんですが。一方、僕は知ってる人は知ってますが大のオブジェクト指向嫌いなんですよ(笑)。やってられっか、とか思って。そこでプラットフォームだけの違いじゃなくってCLOSで書かれたコードをPLTの構造体を使って書き直す、というハメになってしまった。これがハマりまくった原因です(爆)。何やってんだろうねえ、全くもう。



ただ、やっぱりオブジェクト指向は怖いですよ。後でその理由もちょっと考察してみます。では、感想文モドキのメモ。



PLTの構造体は単一継承が出来る



PLTは独自の構造体を提供しています。R6RSにも準拠してないですし、またSRFIにも準拠していない、どちらかと言うとCommon Lispの構造体定義法に近いdefine-structというデータ定義用マクロを提供しています。


このブログでも紹介しましたし、こちらのブログでも紹介されていますが、define-structは大変使い易いです。っつーか、例によって、例えばR6RS構造体のベースになったSRFIで提案されている構造体定義のドキュメントがワケワカメで使う気にならん、って事もあるんですけど。一般にSchemeのドキュメントってあんま良くねえよな。読みづらくって。しかもレコード型とか言ってるのが気にくわん(笑)。Pascalじゃねえだろっての(笑)。Common Lispと違いを出せばいいってモンじゃねえ(笑)。


さて、このPLTの構造体。単一継承が可能です。親クラスっつーか親構造体っつーか。何て呼べばいいのか知りませんが、いずれにせよ、継承可能です。ただし。多重継承は出来ません。つまり一本のラインで連鎖しているデータ型が定義出来る。


まあ、元々CLOSで書かれたネタをdefine-structで解決出来るのか、というような興味がそもそもあったわけなんですけれども。元ネタの場合にも「これは無駄な継承なんじゃないか?」ってのがチラホラあって。全体的には納得してないんですよね(笑)。


今回の場合、根本的にはアイデンティティ(同一性)の判定、つまり根幹のeq判定の為にオブジェクトと言うルート構造体を作ってるわけです。IDナンバーを振り分ける為だけの。Common Lispだと、本当は、パッケージに属してるシンボルが指しているアドレスのポインタ比較でeqが成り立ってるわけなんですが、そこをIDナンバーを製造する為だけのオブジェクト型にすり替えています。要するに手抜きですよね(笑)。全ての構造体の実体には全く違うIDナンバーが振り分けられるシステムなんで、IDナンバーが違えば同一にはならない、というカラクリになっています。


このシステムから言うと、こいつ「だけ」を継承すれば良い筈なんですが、生憎対話によるCommon Lisp入門 POD版ではCLOS使いまくりでした(笑)。弊害は番組後半で(謎)。



マクロの使い方が間違っているけど(笑)、それでもmake-instanceは超便利だった



今回はたった一つだけmake-instanceという名前のマクロを作成しました。make-instanceなんて名前は丸っきりオブジェクト指向っぽいんですけどね。だったらはじめからオブジェクト指向で実装すりゃあいいのに(笑)。いやいや。


これの作成理由は二つあります。一つは対話によるCommon Lisp入門 POD版テキスト内のコードとの整合比較を簡単にする為。もう一つは最終的には全部「全く違うIDナンバー」を継承する必要性の為、IDナンバー込みで実体を自動作成したほうがラクだろ、ってのがあったんですよね。


さて、そんなワケで、今回のマクロmake-instanceは普通使い道が無いんじゃねえの?と思われているsyntax-rulesキーワード引数が大活躍しています。ってかこれは使い方としては間違ってんだろうな(笑)。しかし、パターンマッチング型のsyntax-rulesだとこういう使い方もアリじゃねえのか、とは思うんですけどね(笑)。上手く行ったからまあエエか(笑)。CSの宿題のcond作成の為のelse突っ込む場所なだけ、ってのも勿体ねえからな(笑)。



マクロを大量に使う場合は、ソースコード上に置く位置に気を付けて



define-structもマクロです。make-instanceも作成したマクロです。



Schemeの場合、Common Lispみたいなあからさまなeval-whenに纏わるトラブルは起きませんが。その代わり、あるマクロを使用した手続きがある場合、マクロ定義をファイルの先の方に置いておくのは鉄則です。その辺はインタプリタっぽい「お約束」があるので気を付けましょう。これは実装提供のマクロでも注意が必要です。



Lispのリストは便利過ぎる



シーケンシャルアクセスであるリスト(C用語で言うと連結リスト?)は遅いし重い、って印象があるわけですよ。従って、実践主義者であるPeter Seibelなんかは「Common Lispは遅くない」という事を言いたいが為にわざと、著書実践Common Lispでは、戦略的にリストの説明をあとの方に置いています。「Lispはリストだけじゃないんだ!」と言う為に。


LispのLispたる所以はリスト操作の快適さ、なわけなんですけれども、反面「リストの為に」遅い、重い、という印象になってるわけです。


余談ですが、Common Lisp以前のLispでは、何と本当に関数が内部構造をリストとして表現されていたとの事。ぶっちゃけ、重そうなんですが、反面、関数定義式がすぐ見れると言う利点もあった。Common Lispでのシンボル型へのアクセサの一つ、symbol-functionってのは元々はこいつを引きずり出す為の関数だった模様です。そして当時の実装で有名だったマクロにppと言うものがあり、この内部定義での関数のリスト表現を清書印字してくれました。これも当時のsymbol-functionあっての機能だったんですね。


しかし、これじゃ重すぎるんで、Common Lispでは関数のリスト表現を止め、もっと低レベルへのコードへとコンパイルする道を選んだのです。全ては効率の為に。代わりに、Common Lispではdescribeやらfunction-lambda-expressionやらを仕様として要求するようになったんですが、かと言って、当然文書フォーマットまで仕様で定義されているわけじゃあありません。従って、これで「読める」詳細は実装次第だ、という事になります。昔のLispなら実際定義されているコードが眺められたわけですけれど、実際動いているコードがどういう風に書かれているのか、ソースを見なくても分かる、という利点は永久に失われてしまったのです。ああ。


何が言いたいのかというと。効率と表現力はトレードオフの関係にあるという事です。効率を優先すると表現力が落ち、表現力を優先すると効率が落ちる、という事です。どうやらこれは真理らしい。少なくともLispに於いては。


依然としてLispのリストは便利過ぎる。プログラミング言語をLispからマトモに触り始めた自分は特にそうなんですけど、リストがあれば他に何もいらねーやってくらい他のデータ型を必要としないんですね(笑)。何でもリストでやれるわけですし、極めて柔軟なリストを使うと、例えばランダムアクセスの配列とか使う気がしなくなってくる(笑)。まあ、それじゃいけないのは百も承知なんですが、でも便利。何でも突っ込めるしアクセス方法も簡単ですから。


まあ、そういうテイストの矯正の意味もあって、敢えてハッシュとか構造体とか使ってみよう、って課題があったんですけど。まず効率が増える度に自由度があからさまに減っていくんですよ(笑)。これは痛感しました。多分データ型の極北であるオブジェクト指向なんてのは僕から言わせれば不自由の極みですね(笑)。


一体何が起きてるのか?つまり、データ型には設計が必要なんです。リストは何も要らね(笑)。ところが、データ型の設計の際に個人個人の頭の中に「システムの全体像」がある。そこを簡単に他人が修正するわけにはいかない、って事なんです。もちろん自分で設計したもので自分が改良するのはラクですが、たとえ教科書と言おうと他人の褌で相撲を取るってのは至難の業。いや、もっと言うと他人のデータ設計でプログラムを改良するのは無理だ、と言う結論に到達しました。まあ、僕の力量不足もあるんですけどね。


これは前回のSmall-Lispなんかは、他人の設計でも「自由度の高い」リストで主に組み立てられていたからこそ、ちょこちょこと色んな僕なりのアイディアを試せたわけです。一方、今回は難攻不落のオブジェクト指向が元ネタです。誰かがデータ構造さえ決まればプログラムは自然と決まるとか言ったらしいんですが、逆に言うと、データ構造を変更すればプログラムの全体を手直ししなければならないという意味でしょう。データ構造の手直しはちょっとやそっとでは出来ないって事に他なりません。リスト弄りと次元の違う難しさになります。実際、色々と改良を試みましたがことごとく失敗致しました


まあ、今回はテキスト準拠だ、って前提があるんでこれでもいいんですけど。ただし、自分で設計する場合、どんな言語を使おうと、最初のプロトタイプは効率が悪くてももっとも柔軟なデータ型で作れってのが鉄則のような気がします。どうせ書き直すんですから(笑)、純粋に自分のアタマの中にあるロジック「だけを」試せるデータ型を選んだ方が良いです。そして動かしてみて徹底的に利点、問題点を洗い出す。最初から効率的なデータ型を選ぶと恐らくドツボです。それは恐らく早すぎる最適化と呼ばれるものでしょう。


もう一回言いますが、効率は自由度とのトレードオフで得られるものだと言う事です。C言語なんかは自分で連結リストを実装しなければならないでしょうが、恐らくその方が遥かにマシな結果になるような気がします。気がするだけですが。


ちなみに、ポール・グレアムは次のような事を書いていました。




リストはとても柔軟なので、探検的プログラミングにおいて有用である。リストが何を正確に表現するかについて事前にコミットする必要はない。たとえば、平面上の点を表現するために二つの数のリストを使える。二つのフィールドxとyをもつ点オブジェクトを定義する方がより正しいと考える人がいるかもしれない。しかし、点を表現するためにリストを使うと、n次元を扱うようにプログラムを拡張するときに、行なう必要があるのは座標がない場合はゼロをデフォルトとする新たなコードを作ることだけであり、残りの平面に関するあらゆるコードは動作し続けるだろう。

あるいは、別の方向に拡張して部分的に評価された点を許すと決めた場合、点の成分として変数を表現するシンボルを使い始めることが可能であり、そしてまた、既存のコードは動作し続ける。

探検的プログラミングでは、早すぎる仕様を避けることは早すぎる最適化を避けることと同じくらい重要である。




ポール・グレアムがArcを実装する際に数さえリストで表現しようとしてみたのは割に有名な話だと思うんですが、これも自由度を優先した実験だったのでしょう。ある意味今は、「効率化」はプログラミング言語の仕事であるより、ハードウェアの進歩の仕事になってきてますからね。



さて、いよいよ本題。色々とオブジェクト指向への文句を書いていきます(笑)。



CLOSはオーバースペック



CLOS(Common Lisp Object System)程賛否両論のオブジェクト指向システムもないでしょう。いや、賛否両論ってのはちょっと違うか。実はANSI Common Lispが一番最初に公式仕様としてオブジェクト指向が定義された言語である事に誰も異論は挟まないようですし、また、誰もがCLOSがANSI Common Lispの基盤を支えてる、ってのは分かっているらしい。


ただ、要するに、CLOSを積極的に使う派と全く使わない派に分かれているってのが面白いトコです。まあ、カッコいい言い方をすればマルチパラダイムを体現しているって事なんでしょうけれども。


ところで。何で対話によるCommon Lisp入門 POD版ではCLOSでCommon Lispのサブセット実装、と言うネタにしたんでしょうか?想像するにページ数が足りなかったんじゃねえの、って思ってるんですが(笑)。


同書ではCommon Lispでのデータ抽象の話->オブジェクト指向の話->Common Lisp実装、と速いペースで流れているわけですが、殆ど本の最後なんですよね(笑)。んで、どうせオブジェクト指向を紹介しているわけですし、ページ数も少ないんで、いっちょCLOSでも使ってサブセット実装してみせるか、と。ページ数が足りないんで。ここって大事ですよ。


つまり、制限がキツいページ数でどうにかマトモに動くシステムを作るにはオブジェクト指向って強力なんですよ(笑)。特にCLOSは桁違いのパワーを持っている。色んな意味で舌を巻きました。恐らく普通に説明して実装する流れだとあのページ数でここまで動かせないんじゃなかろうか、と。CLOSのパワーを感じた瞬間です。だからこそ問題があると思ったわけなんですけど。



なお、元々CLOSはSmalltalkと言う言語に影響を受けて誕生した模様で(この辺、Simulaの影響を受けたC++/Javaと対照的)、またルーツを鑑みれば分かるんですが、そもそもLispマシンでのGUIフロントエンドを作成する為に作られたシステムだった模様です。要するにザックリ言うと、ある種コンピュータ・グラフィックス専用のシステムなんです。従って、それに関しては多大な力を発揮するように作られていますが、同時に、文字処理ベースのシステムに対してはオーバースペックって言っても良いかもしれません。つまり、スタイル的に色々とおかしな問題がこのレベルでは出てくるんじゃないか、って思います。



CLOSではデータ追加は超簡単、反面読み解くのが難しい



対話によるCommon Lisp入門 POD版の流れから言うと。最初にIDナンバー特定の為のルートクラスとしてオブジェクトクラスを作成。その子クラスとしてアトムクラスとリストクラスを作成しています。そしてアトムクラスを継承してシンボルクラスを作成。そしてシンボルクラスとリストクラスを多重継承してLisp唯一無二のデータ型であるnullクラスを作成します。と言うのも、nilはCommon Lispではアトムでもあるしリストでもあるから、です。これにより、両者を継承しているnilクラスは自然とアトムにもなりリストにもなる。凄く軽快にデータ型をここまで追加していきます。オブジェクト指向って何て便利なんでしょう!!!…ん?


いや、これは変なんですよ。確かにLisp初心者にはアトムとリストは対になってる、と教えます。教えますが……。実はこれらは本当は対立していません。アトムと対立してるデータ型はリストじゃなくってコンスなんです。従って、この論法から言うとnullはシンボルを継承しても構いませんが、リストを継承する必要なんてない、のです。単にルート近辺の継承クラスにアトムとリストを設定したからおかしな事になる。


ここで言いたいのは、継承を有効活用する以上、データ型のヒエラルキーはキチンと設計しなければならないと言う当たり前の話です。当たり前の話なんですが……CLOSはあまりに強力過ぎて、多重継承があまりにも簡単に行えてしまう為にデータ構造の設計ミスをいとも簡単に隠蔽出来てしまう、のです。残るのはこんがらがったヒエラルキーとは呼べないデータ階層の残骸です。お互いリンクを貼りまくってわけが分からなくなる。


CLOSはかなり危険なシステムなんですよ。自重する分には構わないんでしょうが、あまりにも簡単にデータ型同士にリンクが貼れてしまう為に後に構造見直すには大変な思いをする事になるでしょう。実際、この階層をPLTの単一継承での構造体のリンクへと直すのに若干困りましたから。規模にも当然依るでしょうが、このレベルでこんがらがる、って事は大きなシステムだったらなおさらこんがらがるだろう、って事です。



オブジェクト指向は破壊的操作がいっぱい



まあ、これもスタイルの問題なんでしょうけどね〜。根本的にオブジェクト指向の場合、フィールド(あるいはスロットの値)を破壊的に書き換えるのが前提の為、Schemeが標榜する関数型プログラミングと真逆のトコに位置しています。いやあ、こんなにSchemeで破壊的操作ばっかしたの初めてかもしれない(笑)。もっと上手いテ使ってイミュータブルに仕上げられる可能性もあるんだろうけどな〜。CLOSが前提で書かれたテキストなんで、この辺、上手いテが思いつきませんでした。っつーか先程書いた通り、データ構造設計が決まれば自ずとプログラムが決まるなら、このヒエラルキーが決定された以上、逃げようがないんですけどね。いや、参った参った。



この実装だと中身は循環構造でいっぱい



んで破壊的操作を目論むと当然中には循環構造なんかが自ずとから出てくるわけですよ(笑)。代表的なのはシンボルnilnilsymbol-valuenilなんですよね(笑)。これで如何にも簡単に循環構造の出来上がり、です。破壊的操作様々っつーか(笑)。ポインタが自分自身を指している(笑)。


あと、パッケージもひでえな。パッケージに登録されたnilは当然シンボルなんで、パッケージ内で循環してる、と言う(笑)。最初、構造体で#:transparent(透過設定)付けてたんですけど、あまりにリンクがヒドイんで、見たくなくなって止めました(笑)。ハズしちゃった(笑)。


これ、本物のCommon Lisp実装ってどうなってんだろうな〜。あんまCommon Lispじゃ表面的に循環構造出さないんで。興味があると言えばあるんですけど(SBCLでは*print-circle*弄っても特に循環してませんでしたけどね)。いや、こええこええ。最近のSchemeだとこう言う感じの構造を作るって推奨されてねえんじゃないですかい?



メソッドがあっちこっちに散らかりまくり



恐らく他のメジャーな言語でオブジェクト指向をやって、CLOSでビックリすんのは。メソッドがクラスに属してないって事なんじゃないですかね?普通、オブジェクト指向の単純な説明では「データ型と関数を一緒にしたもの」って解説されているわけですけれども。Common Lisp Object Systemではクラス内で特にメソッドを定義しない。じゃあ、どこがオブジェクト指向なの?となるわけです。


CLOSではメソッドは総称関数と呼ばれる特殊な関数に属しています。しかもこいつは暗黙で作られる(もちろん明示作成してもいいわけですけど)。従ってクラスでデータ型定義、メソッドはまた別に、ってなるわけです。んじゃ何かこのレベルでメリットがあるのか?と問われれば実際は何もないってのが本当のところで(笑)。


唯一目立ったトコですと


  • defmethodとはクラス特有の関数が作れる事

  • メソッドは表面的にはクラスに総称関数を通じて関連付けられているので場合分けする必要がない


ってトコでしょうか。平たく言うと同名異機能の関数が定義出来るって事になるんですけど……。う〜む。



例えば、対話によるCommon Lisp入門 POD版によるとだ。evalなんてのもメソッドとして定義されてるわけですよ。あるクラス専用のeval、別のクラス専用のeval、と。



そうなると、ソースコード上にあっちにエバるは、こっちにエバる、はものすごく散漫な印象になりますよね(笑)。どっかにまとめて置いた方がいいんじゃねえの?と。でも、どっかにまとめるんだったら最初からcondでも使って単一関数として定義しても同じなんじゃねえか、と言う(笑)。何じゃそりゃ(笑)。



まあ、多分、こういうシステムにしてるのは、それこそやっぱりコンピュータグラフィックス作成みたいな大規模システム向けだから、って事じゃないんですかね?まともに書いてて条件分けが20とか30とかになったらさすがに嫌だろ、って事で(笑)。だったら、そのテの条件分け自体が判定システムである総称関数に任せておいて、クラスの傍にメソッド置いときゃエエやん、と。多分元々の発想はそんなトコだったと思いますけど。



要するに、この程度の分量のソースコード量だとCLOSなんて出してもしゃーないって事でもあるんですよ。どの程度の規模のシステムから上がCLOSの領域になるのか、ってのはハッキリしませんが、いずれにせよ、この程度の分量で、あっちにメソッド、こっちにも同名のメソッドが分散してる、って感じじゃ単に邪魔なだけって気がします。古き良きcondを利用したほうがスッキリすると思います。



なお、PLTの構造体を使って場合分けする場合、継承が絡んでくるので、より特定的な派生データ型から条件を徐々に緩めていくような書き方をしないとなりません。最初の一番目の述語がルート構造体のモノだったりすれば派生構造体は全部#tになっちゃうんでおかしくなる。まあ、その辺総称関数だとある程度指定しつつも、自動で最も特定的なクラスに絡んだメソッドを選び出してくれる、ってんでラクってばラクなんですけど……。そもそもそんな事態に陥るのは継承なんて余計な事をしてるからじゃねえのかってのも事実であって(笑)。やれやれ(笑)。



ま、いずれにせよ、CLOSは大規模システム向けです。個人レベルだと使おうが使わまいがどーでもいい。っつーかこのレベルでは明らかに害悪なんじゃねーの、とか思います。



CLOSに付いては以上。終わり。



継続はやっつけ仕事のハックにも最適



クダラない話なんですが(笑)。対話によるCommon Lisp入門 POD版はあまりにも教科書的なREPLモデルを採用していて。これが一旦上部レイヤーのインタプリタ内部に入ると止められないんだ(笑)。端末クローズする以外手がない、っつーか(笑)。どうすんだろ、これ、みたいな感じで(笑)。他の人の話聞いてみたい(爆)。



重要なのは。このシステムだと、一旦READしたら即レイヤー上のLisp構文に変更しちゃうわけですよ。そのデータ変換ってのがこのテキスト上のCommon Lispのサブセットの肝なんですけど。一旦変換されたらexit命令もそのレイヤー内で実装しなければならない、って事になる。一体どうすんべえ、と。かなり困ってたんですね(笑)。



そこでやっつけ仕事。伝家の宝刀、Schemeのcall/ccの出番です。連続体(print (eval (read)))は切り離せないんで、READの返り値がある条件を満たした時、これらカッコの連続体から大域脱出しちゃう。これならちょっとの変更だけで、構造壊しませんしね。やっつけ仕事の際のcall/ccはマジで重宝しますよ(笑)。もう大好き(笑)。



マクロ実装はやっぱり今後の課題



かなりマクロ実装まで後一歩、ってトコまで来てる感じもするんですけどね〜。実際、内部定義してる時「これって形式的にはマクロだよな?」と思いまた、「マクロさえあればもっと簡単に定義出来るのに…」とほぞを噛む事もしばしば、でした。


Lisp系の本では、良く、マクロによるメタプログラミングの重要性が解かれています。ポール・グレアムは次のように言ってます。



マクロを書くというのは、Lispのプログラミングの中でも特別な手法であり、固有の目的と問題がある。コンパイラに渡る内容に変更を加えられるというのは、コンパイラを書き換えられるのとほとんど一緒である。したがって、マクロを書くときには、言語設計者のように考えながら始めなければならない。




ぶっちゃけ、


「何この人言ってんの?」

とか思ったわけですけど(笑)。メタプログラミングやるのにあたって、プログラマが言語設計者のように考えなければならない?言語設計した事もねえのにか(笑)。だったら無理だろ、と(笑)。



いや、その通りなんだと思います。プログラマにメタプログラミングは通常要らないんですよ。歌うたった事ないヤツに「歌手のように考えろ」とか、演技した事無いヤツに「俳優のように考えろ」ってのは土台無理な相談でしょ。プログラムした事ないヤツに「プログラマのように考えろ」ってのはメタファとしては成り立ちますが現実的な提案じゃありません。しかしマクロは現実に存在してて現実に関連しているわけですから、別な言い方するとやっぱり「言語設計した事が無いヤツが言語設計者のように考える」ってのは無理なんです。んな事ありえない。


という事はだ。この提案には次の二つの意味が考えられます。



  1. 実はマクロ自体は言語設計者に取って一番便利な機能であって、ある意味プログラマの為のものではない。

  2. マクロを理解するには実際にプログラミング言語を実装してみる必要がある。



この二つですよね。そして、これらはLispに於いてはかなり接近している、って意味なんです。つまり、Lispの力がマクロにあり、他の言語に比べるとLispのLispによる実装が簡単だとしたら……。試してみる価値がありますし、恐らく自分でLisp実装をしてみるのがマクロを理解する早道でしょう。そして実装した途端マクロが絶対欲しくなる。コア機能を定義した後のライブラリ的な意味じゃない「機能拡張」はマクロがあったほうが俄然ラクだから、です。


多分、この辺が、Lispのマクロに関しての他言語ユーザーの誤解の源なのかな、とはちょっと思いますね。その辺の言語でその言語自体を実装する事自体が凄く難しい。Lispは機能拡張可能な言語ですが、それは言語設計との境界線を曖昧にしている。それが故にLispに於ける「メタプログラミング」の重要性がピンと来なくなるんで、マクロ是非論、ってのが出てくるのかも。


もう一回言うと、マクロは言語設計者の為の機能でプログラマの為の機能ではない事は明らかです。ただし、Lispに於いてはプログラマと言語設計者の境界線は曖昧だって事です。という事は曲りなりにもLispを実装してみる価値はある。


多分プログラマ的観点でいてもつまるところマクロって理解出来ないような気がします。何となくですけど。で、Lispに於ける境界線が曖昧だったら跨いでもエエんちゃうの?ってのを最近感じ始めました。向こうの水は甘いぞ(笑)。


まあ、残念ながら、今のトコ、defmacroのプリミティヴとしての実装方法って分からないんですけどね。もうちょっとLisp実装を何回かやってみて、改めてArcのソースでも精読してみたいと思います。何かヒントがあるかも。



とまあ、これらが徒然と今回感じたもの、です。CLOSでのコードには腹立ちながらやってたんですが(笑)、曲りなりにもパッケージシステムの実装を通じて、Lisp-1でLisp-2が実装出来たんだ、ってのは感慨深いです。ちょっとレアケースかも、とか思っています。まあ、まだ色々と抜けもありますが、いずれにせよ、前回のSmall-Lispに比べても高機能になったんで良かったですね。苦労しただけの事はありました。






2010年5月16日日曜日

vallog: macro while

vallog: macro while


do マクロの使い方を覚えられないクラスタです。


なかなかこれが、Schemeでは再帰ばっか練習するんで、doは意識して練習しないとdo嫌いに成りかねません。
まあ、以前も言ったんですけど、スタイル的には実はdonamed-letの変種です。Common Lispの内部では再帰とは全く違う破壊的な計算を行ないますが、かと言って、スタイルだけに注目すれば、実はnamed-letとはそんなに違いがないのです。
僕の中では

  1. 横に広がるnamed-let

  2. 縦に伸びるdo


とか言ってました。意味分かんないっすね(笑)。まあ、変数束縛の位置が、って事なんですけれども。あと、doがフグみたいに見える、ってんでフグ構文とか呼んでたりしました(笑)。

doの一般形は次のようになっています。



ね、フグみたいでしょ(笑)?



Schemeのnamed-letの一般形は次の通りです。



つまり、基本的には変数、初期値、ステップの配置が違うだけ、です。doだとまとめて記述する。named-letだとそうじゃない、って事ですね。

どっちがどっちより便利、って事は基本無いわけなんですけれども。



ただ、個人的には、ここで言う副作用とは、例えば入出力であるとか、そう言うケースですが、副作用が関わる式、特に出力を実行しなければならない場合、doの方がよりシンプルに書ける場合が多いとは思います。named-letだと、スッキリ決まらないケースが多くて、そう言う場合、doの方がシックリくる場合が多いような気がします。



上の例はあまり良くないんですが、と言うのも、named-letcondを使って暗黙のbeginで上手い具合にみっともなさを回避してるんですが、基本、named-letのシンプルさはぶち壊れてるんです(笑)。目立ちませんが(笑)。
反面、後者のdoヴァージョンは本体部に「計算以外の余計な作業」をまとめられます。これはシンプル過ぎる例なんですが、長く余計な作業がある、って事はままあるんです。そう言う時、さすがの再帰構文でもシンプルに書けなくなる。いや、構造はシンプルなんですが、コード自体は汚く見える場合があるんですよね。

いや、すまない。いい例が思いつかなかった(爆)。ただ、普段は再帰で構わないけど、いざとなったらdoの方がシンプルに書ける場合があるんだよ、って事です。はい。


注:いや、ホントに下手な例でゴメン。というのも、doに関して言っても、カウンター内で出来る事は全てやってしまうのがスタイル的には美しいんですが、それで悩むんだったら素直に再帰した方が良い、ってのが事実。要するに再帰での「引数内のカウンター処理だけじゃどうしようもない」部分が出てきた場合、doの出番だ、って言い方の方が正しいかも。


さて、valvallowさんの記事によると、どうやら「NILじゃない返り値が欲しい」との事。まあ、これは当然でしょうね。
ポール・グレアムが何でマクロで書いたwhileNIL以外の返り値を返すようにしなかったのか?想像するに、二つ程理由が考えられて、マクロの導入章(第7章なのに!)の辺りなんで、あんまややこしい例じゃなくってシンプルな例にしたかった事。あとは破壊的変更が前提のマクロなんで、返り値を返すとマズイ、って事があったんでしょうね。

グレアムのwhileは次のようにしてみれば面白い結果が出てきます。



whileの指定はi < 10の筈なのに、何とiは10になっています。つまり、返り値になるのはこのiになるのは自明でしょう。これはマズいです(笑)。恐らく返り値は9であって欲しい、ってのが皆願う事でしょうから。
だから破壊的変更が前提なら、返り値をNILでもしとけば良い。堅実な判断ですよね。

さて、それでは返り値を、Common Lispらしく最後に実行された値を返すように改造していきましょう。まずは、LOLでお馴染みでしょうが、関数lastbutlastってのを使ってみます。



関数lastはリストの最後の値のリストを返し、関数butlastはリストの最後の要素を除いたリストを返します。今何でこれを使おうと思ったのか、と言う理由は、繰り返しが何回実行されようと、必要なのは計算の最後の作業結果だという事だからです。
例えば、上のvalvallowさんの例だと、欲しいのは式をして実行された(print i)(incf i)全体じゃなくって、あくまで(incf i)だけ、なんです。言い換えるとbodyの最後の計算結果さえ分かっていれば良い、と言う事です。
すなわち、bodyを二つに分けちゃう。最後と、それ以外、です。

このアイディアで雛形は一応次の通りになります。



では実行してみますか。



やっぱりダメだ(笑)!bodyを二つに分ける、ってアイディアは秀悦だとは思うんですが、返り値が10になってます。こりゃイカン。
この理由は、終了条件が調べられる前にvarが更新されちゃうから、なんです。つまり、テスト式に行く前にiが10になってしまう。ここを直さないとどーにもならん、わけです。
さて、ここでやっつけ仕事のハック。嫌でもvarが更新されちゃうんだったら、その前の状態を保存しとけばエエんちゃうの?ってのがアイディア。つまり、変数を二つ用意しちゃうんだ!!!一つは今まで通り(last body)の更新用。もうひとつは、前回のそれの保存用だ!これでどうだ。



では実行してみましょう。



上手く行きましたね。大成功!!!これで終わり……とはいかないんですよ、残念ながら(笑)。
実はこのマクロは次のような問題があるんです。



これが変数衝突ですね。つまり、my-while%%マクロの内部でvar0var1って変数名を使ってるわけなんですけど、これが外部から与えられると途端にぶつかってしまってどうにもこうにも行かなくなる。
つまり、これを避けるのがgensymです。だから、こう書かないとなりません。



これで安心してどんな変数名をmy-whileマクロ内に持たせても大丈夫です。



gensymをいつ使うか?基本的にはいつでもです。意図してアナフォリックマクロを書く時以外はバンバン使って構わないと思います。あって困るもんじゃない。
基本的には、

  • defmacroで引数として与えられた変数以外で、テンプレート内に「いきなり」現れる名前全部に対してgensymを使って構わない


と言うのが原則です。複雑なマクロでテンプレート内にバンバン新しい変数が現れる場合はgensymだらけになりますが、それで良い、のです。繰り返しますが、gensymはあって困るもんじゃない。むしろ無けりゃ困るんです。

まあ、もっともみっともなくなる可能性もありますが、その回避の為にOn Lispではwith-gensymsというコードが紹介されています。また、上のmy-whileはLOL流にdefmacro!を用いれば次のように記述されますね。



ちなみに、再帰版は次のようになるでしょう。

2010年5月14日金曜日

vallog: On Lisp memoize

vallog: On Lisp memoize

あああ、僕もうっかりしてました。

On Lispのmemoizeなんですけど。久々にGauche起動してValvallowさんに従って試してみたら、次のような結果になりました。



あれ、全然速くなっていない……。

考えてみれば当然で、手続きmemoizeは呼び出す度に新しくレキシカル環境を作ります。
と言う事は。cacheに保存される筈のハッシュテーブルは、手続きmemoizeが呼び出される度に新しくクロージャの中で作られて、そしてmemoizeの起動が終了する度にその寿命を全うするわけです。
従って、hash-table-getの返り値は毎度必ず#fになる、って事です。

これじゃあ意味無い。

んで、On Lispを良く見てみると、実は「敢えて」memoizeとそれが受け取る関数引数を合わせて大域環境に束縛しているんです。
これが手続きmemoize正しい使い方です。本に依ると、大域変数slowidに束縛されている以上、クロージャ、翻って使用されたハッシュテーブルが廃棄されません。



ご覧になれば分かるでしょうが、一回目の大域変数slowidに35を付けた時の呼び出しは遅いです。反面、クロージャ/ハッシュテーブルは依然と大域変数slowidに束縛されたまま、なんで、二回目の呼び出し時は高速なハッシュ検索により、時間が殆どかからず値を見つけ出している事が分かると思います。

これがmemoizeの使い方です。

2010年5月12日水曜日

Small-Lisp 実装を通して

感想文です。

まあ、前言った通り、

「Lispの実装をLispで行う?ネタ?それってオモロイのか?」

とか懐疑的だったんですけど。実際やってみると面白い(笑)。ハマりました(笑)。色んな意味ですけどね。

まあ、別に自分で0から考えて、ってわけじゃなくって、森北から出ているSchemeによる記号処理入門を見ながらやってたわけですけど。それで自分が気に入らないコーディングスタイルとか、あるいはこうだったら良いなあ、ってのを考えながら弄ってたわけですけれども。

でもマジに意外と面白かったです。ちょっとはSICPの第4章辺り読む自信付いたかな?

個人的には「こりゃ大変だ」とか思ったのがラムダ式(Small-Lispではfnと言う名前になってる)と=(Common Lispで言うsetf)の実装ですかね。あ、ifも大変だったな。中でもやっぱ一番大変だったのは=の実装ですかね。CLのsetfみたいな動作させたい、ってんで、CLでmacroexpandかけて調べてみたんですが、これが出てくるのがprimitiveであるrplacarplacdで。あんまこの二つ使った事が無いんで、Common Lisp 入門 (岩波コンピュータサイエンス)調べながら…と一番手間がかかりました。
結局、大域環境である*environment*をPLTのハッシュで実装してたんで、そこの内容をhash-set!で無理矢理書き換える、と。そう言う荒業を行って何とか解決。alistだったらこう上手くは行かなかったんだろうな。ハッシュテーブル様々です。

まあ、課題が多い実装なんですが。今思いつくトコをちょっとメモ。

  1. ラムダ式のbodyに複数の式が置けない。

  2. レキシカル・クロージャはどうやって実装する?

  3. 一々quoteとか書きたくないんだけど、ショートカットである'の実装方法は?

  4. マクロの実装方法は?


ってなトコですかね。
本の進み方によると、これはダイナミック・スコープにしかなり得ないと思うんで、そこの回避が謎ですね。まあ、この辺はSICP再挑戦して読んでみればわかるのかな?
quoteもみっともないですね。リーダマクロを実装すれば何とかなるのでしょうか。でもそこへ行くのが大変そう(笑)。
最後のマクロ実装。これは痛感しました。実は何を置いてもマクロを最初に実装しておくべきなのでは、と。
例えば、Small-Lispだとdefが特殊オペレータ扱いになってるんですよね。ところが、=fnがあるんで、マクロさえあればその上に被せたレイヤー上に簡単に実装出来るわけです。マクロが無いからevalを直接弄って特殊オペレータとして実装しなきゃならない。これは無駄ですよね。
fnを実装している最中に「あ、これはマクロ書いてるっぽいな」とかちょっと思ったわけですけど、要するに直接evalを弄らずにショートカットできればいいのに。マクロは偉大です。自分でLispを実装してみるのがマクロ理解への一つの方策なのかもしれません。
が、マクロ実装を解説してあるような本ってあるんか(笑)。

その辺でcond実装するのも止めちゃったんだよね(笑)。マクロとifがあればcondなんてお茶の子さいさいで実装出来るのに。スカンタコ。

でもまあ、マジで面白いですよ。自分でプログラミング言語を曲りなりにも実装する、ってのは自信に繋がりますね。面白い。んで、ポール・グレアムのArcのコードも参考にしてたんですが、手探りでやってても色んな事が徐々に判ってくる。

ところで、CSの宿題で「LispでLispを実装しなきゃいけない」って言われて嫌々やってる人たちにちょっとヒントを。

applyevalの仕事の区別なんて付けないで良い


いきなり怒られそうな事を書いてるんですけど(笑)。でも、やってみてここはそう思いました。
SICPなんかでは「Lispはevalapplyの相互作用」とかエラソーな事書かれてたんですけど(笑)。実際、この辺は実装者のさじ加減でしょうね。
つまり、やりたけりゃ、特殊形式であるconsとか、特殊形式であるcarとかやってもいいわけです。「やっちゃいけない」ってワケじゃない。まあ、Lispのオーソドックスな実装上の流れから言ってevalapplyに分けてる、って考えて良いでしょう。
一般には、次の役割がそれぞれあるわけですよ。

  1. 特殊形式はeval内で実装する。

  2. 基本関数はapply内で実装する。


これは要するに、REPLで打った時、引数にクオートが要らないのが前者、引数にクオートが必要になるのが後者、です。それだけ、なんです(笑)。マジな話で(笑)。
つまり、REPLで打った時に「引数いらねえ方が俺は好きだな」って場合は、堂々とeval内で実装しちゃいましょう(笑)。貴方のLispである以上、貴方の好みで良い筈です。それが例え「スチャラカな」実装だとしても。
実装形式上の話をすると、evalexpressionを丸ごと一つとして受け取りますが、codeは分解された状態(function部とその引数部)で
expression
を受け取ります。このささいな違いがREPL上の引数の動作に影響を与える、のです。

メンド臭い実装はベースのLispに丸投げしちゃおう(笑)



これもそうですね。いくら「自分で実装する」とか言っても、基本的な関数で実装がメンド臭そうだったら、レイヤーの下で動いてるLispに丸投げするのも手、です。全部自分で造らなくても良い。もし、それが必要なら、アセンブリで実装する以上の事ってないわけですから。せっかくLispでLispを実装するんなら、余計な部分は下部で動いているLispに丸投げするのも手、です。恥ずかしがる必要はありません。
なお、ポール・グレアムのArcも読みながらやってたんですが、ポール・グレアムも面倒な部分は下部のPLTに丸投げしてました(笑)。Arcのソースコード内で逆引用符が使われているリストの部分は、どうやら下部のPLTに丸投げする為のハックの模様です。

caseを使いこなそう


どうも、あまりにもcondが優秀なのと、ミニマリズムのせいで、ついついSchemeでは存在が薄れがちなcaseですけど。ソースコードを短く、かつ、読みやすくする為にはcaseの多用が必要だ、と感じました。evalapplyもコードが大きく成りかねないんで、それを避ける為には、

  1. 手続きの分割

  2. caseの多用


が肝です。
そもそも、Emacsの画面の半分以上を占めるようなコードを書くべきじゃない、と言う気がします。ショートカットになる為だったら色々と試してみるべきでしょう。同様に部分解が簡単に形成出来るんだったらdefine-syntax等のマクロも多用すべきだと思います。
Small-Lispだと、しょーもないマクロを3つ程作りましたが、それにより、コードの短縮化が可能になったんで、まあ良し、と思っています(もうちょっと上手く抽象化出来るかもしれない、って疑念もありますが)。

とまあ、感想文でした。
この続きはSICPに進むか、あるいはArcのソースをCLに移植するか。ちょっと楽しくなってきました。

Small-Lisp Ver.1.3







2010年5月11日火曜日

Small-Lisp Ver.1.2







実行例:

Google 日本語入力 on Ubuntu 登場! その名もMozc

もずくが美味しい季節がそろそろやってきますね。

待ち焦がれたGoogle日本語入力オープンソース版、その名もMozc。なかなか良いですよ。
ビルドが必要ですが、解説通りにやっておきゃまあOKです。
っつーか事実上、端末上へのコピペだよな(笑)。

解説は次のページで。

mozc

結構ビルドに時間かかるんですけどね。
Debian/Ubuntuユーザーは、Compilation飛ばして最後のBuild and install debian packageに飛んでください。CompilationはDebian/Ubuntuユーザー以外へのインストラクションなんで。

それと、多分、インストールしただけではiBusに認識されないと思います。認識させるには再起動かける必要があるのではないでしょうか。

Small-Lisp Ver.1.1







実行例:

2010年5月10日月曜日

Small-Lisp Ver.1.0







実行例:

2010年5月9日日曜日

Arc のソース閲覧

@valvallowさんが


Lisp 系の書籍は、このネタ多いなー(笑)


とか記述していて、全くです、よね(笑)。

この辺のLisp on Lispと言うネタは散見してるんですが、実際僕もそんなにマジメにやった事がないです。マズいんでしょうけどね(笑)。
理由はいくつかあるんですけど、

  • プログラムを作りたいんであってプログラミング言語を作りたいわけじゃない

  • そもそも、それで作って効率的なの?


と言うのが大きな理由でしょうか。例えば、自分でマジメにLisp処理系をLisp上で書いて、それで果たして愛用出来るようなものになるんか、とか。単なるCSの宿題のネタじゃなかろうか、とか。マジメにLisp処理系を作ってる人はやっぱりCとかで書いてるんだろ?と。LispでLispを書くのが面白くて実用的であれば、ブートストラッピング的にこの世はLispで書かれたLisp処理系で溢れてるんじゃなかろうか、とかね。色々謎がある。

現存するLispで「Lispで書かれたLisp」としては、ポール・グレアムのArcがあります。他の言語で書かれたソースだと読めない可能性もある、んですが、一方、Schemeやってる層だったら比較的読みやすいのでは、と。CS的な意味で言っても、非常にオーソドックスな教科書的なコードなんじゃないのかな、って気がちょっとしますね。

いずれ、このソースを楽々読みこなせるようになりたいもの、です。



2010年5月8日土曜日

#9LISP How to use ASDF

What the FUCK is the prerequisite of LOL?



LOLの調子は如何でしょうか?僕は引っかかりまくりながらやっています(笑)。色んな意味で(笑)。



あの本は想定読者がちょっと微妙なところがあります(実際、プログラミング関係の本だとマトモな編集者が介在してるのか分かんないケースが多々あって、想定読者が絞りきれていず、結果、著作者の独りよがりな本を良く見かけますが(※1))。例えば最初の数章が割に冗長なんですけど、先に進んでいくと今度はCommon Lispに精通していなければ意味が分からない部分が散見してたりして。



これ、ホント誤解して欲しくないんですけど、敢えて言っておきます。LOLの評価に関して言うとCommon Lispに精通している人の意見なんて聞いてもしょーがないです。彼らは、そもそも、Lispの本の出版数自体が少ない(C言語/Java/JavaScriptに比べたら圧倒的に少ない!)んで、Lispの本が出ただけで「これは名著だ!」って言いたがる傾向がある、んです。当たり前ですよね。マイナーなアニメのファンブックが出版されたようなモンですから(笑)。中身はともかくとして、ファンなら買って絶賛するよな(笑)。行動パターンがマイナーアニメのファン層と結構カブるので眉に唾付けとかないとなんない(笑)。



第二の問題として、LOLを絶賛しかねない層と言うのは、LOLなんて買わなくてもマクロに精通している層だと言う可能性がある、からです。当然既にCommon Lispに精通しているわけですから、LOLを斜め読みして、




「う~ん、これは良くまとまってるし分かりやすい。」


とか言うでしょう。当たり前です。それは既にCommon Lispの全貌を殆ど把握している人ですから。ところが、こう言う人たちは大体見逃してるんですよね。マクロ初学者に対して適切に構成されてるのか?とか言う視点に欠けてる。「自分が分かれば良い本だ!」とはならないのが難しいトコなんです。本当の事を言うと。



もちろん、ある程度Common Lispを使っていた事がある、と言うのが前提なのかもしれません。しかしそうだとすると、最初の数章の冗長さは何なんだ、って話になる。unit-of-timeマクロなんてあまりにバカバカしい例だと思いませんかね?幼稚な例、と言えばあまりにも幼稚な例です。これ使ってマクロの基礎を解説される層ってどう言う層なんですかね?そもそもCLでマクロを使った事が無い層が対象読者として想定されてなければこんな例ってあり得ないでしょう。



つまるところ、LOLの真のprerequisiteって何なんだって話なんですけどね。LOLはOn Lispを良く引き合いに出してますが、読者的な立場から言うと本当のprerequisiteってのは別のところにある。っつーかそう解釈しないと構成自体に疑問が出ざるを得ない、のです。特に #9LISP みたいに「SchemeからCLに突如移動する」とでもなった場合、この辺の構成の不具合ってのが全面に出てきちゃうのです。



まあ、個人的には、LOLの内容としては、記述されているマクロ自体の価値はともかくとして、本自体は同人誌だとしか思ってないんですけどね。不具合はあって当然(同人誌だから・笑!)なんであんま苛める気は無いんですけど、ただ、権威的な書籍だと思われたら困るだろ、って事だけは言っておきたい。あくまで一書籍として考えた場合、って事です。一方、 #9LISP みたいな勉強会を行ってる建前上、不具合があった方が良い、ってのもまた事実なんですよね(笑)。独学/自習で全て理解出来てしまうような優秀な本だったらそもそも勉強会を開く口実が無くなってしまう(笑)。プログラミングやってる人たちの間で勉強会が多いのは、穿った見方をすると、プログラミング関係の書籍には構成がマズい本がどーゆーわけか多い(※2)と言う事実の裏返しかもしれません(笑)。ちゃうかもしんねえけどよ(笑)。




※1: 実はプログラミング関係の書籍だけ、じゃなくって最近の理工系出版物全般的な傾向なんじゃないか、と思います。編集者が仕事していない

もちろん、

    「編集者は文系出身だから専門的な内容を理解出来ない。」

なんて意見もあるんですが、フザけんな、とか思います(笑)。仮にも編集者はプロでしょうし、そもそもプロの編集者が理解出来ない原稿を書いてる時点で何なんだ、とか思いますし(笑)。だったら紙資源の節約の為、出版なんてせずに論文書いてるべきですね。査定側は同業者でしょうし。わざわざ出版なんかして地球の森林資源を大規模に無駄にする必要もない。

最近の理工系の本がダメダメになってきてるのは、殆ど出版物が事実上ケータイ小説のレベルになってきてる、って事です。だったら新潮社辺りから出版すりゃあエエんちゃうんか(笑)。

※2: SICP、とか(笑)?


Anything relies on ASDF



とか色々文句書いたんですけど(笑)、どう言う経緯を経てそのプログラミング言語を扱ってるのか、と言うのは想定が難しい、と言うのも事実なんですよね。読者が千差万別のバックグラウンドを持っているから、です。



#9LISP のメンバーでもそうかもしれないし、そうじゃないかもしれませんが、CLにあんま明るくない人が最初に躓くのは、恐らくLOLの第3章辺りでいきなり導入されてるCL-PPCREの存在じゃないか、って思います。もちろん、これはANSI仕様書範囲外のトピックです。すなわち、




CL-PPCREって外部ライブラリ?っぽいんだけど…。それはいいとしてどーやってインストールすんの?これ。LOLには何も書いてないじゃん。」


そう。何も書いてない(笑)。いきなり読者置いてきぼりの第一撃が放たれるのです。特にWindows使ってプログラミングしている人(そう言う人の方が多い)は放置プレイですよ。困ったもんだ。



以前、#9LISP LOL 第2章 メモにも書いておいたんですが、こーゆー場合に使うのがASDF-installです。言わばCommon LispのCPANです。まあ、gemでもいいんですけど(笑)。



使い方をおさらいしておきましょうか(※1)。REPLで流れを書いておきます。





2番のPersonal installationを利用すれば良いでしょう。その後、鍵の認証を求められますが、無視して大丈夫です。その後、





とすれば、無事、REPL上でCL-PPCREの提供する全機能を使う事が出来ます。やったね!!!



それはさておき。疑問がある人はあるでしょう。




「オーケー。asdf-installってのはPerlで言うCPAN、Rubyで言うgemなわけね。そこは了承した。何かライブラリをインストールしなきゃなんない、って場合はこれ使えばいいわけだ。何かヘンに括弧があるし、コロンだらけだし、要素が多いんで記述がメンド臭そう(※2)なんだけど一応分かった。

でもさ。そもそもASDFって何なのよ?あとさ。不思議の呪文use-packageって一体何なの?これ無いとダメなのかね?普通はロードしたら即ready-to-rock'n-rollなんじゃねえの?手順が多すぎるんだよな。単にファイル持ってきて手作業でロードした方が良かったりして。」


全くもってその通りです。が、ASDF(Another System Definition Facilities)と言うわざとらしい名前のブツ(※3)が無いとちょっとメンド臭い事象が起こる、って事を書いてみます。現象面から見ていった方が分かりやすいCL独特のメンド臭さがある、のです。




※1: もちろんこれはLinux系OSを使ってない人の場合で、Linux、例えばDebian系ディストロUbuntuだと、レポジトリでCL-PPCREを提供してたりするんで、

sudo aptitude install cl-ppcre

と端末から入力した方がラクだったりする。

また、以前書いた通り、GNU CLISPにはASDFがデフォルトで同梱されてなかったりするんで、最初に別途インストールが必要。ただし、Lispboxの場合、最初からASDFが同梱されているので、面倒が減る。

※2: 確かに長い。ただし、LispboxやあるいはEmacs + SLIMEを利用してる場合は、ライブラリさえインストールしていれば手順は簡略化可能。REPL上でコンマ(,)を打つと、候補が表示される。その中にload-systemと言う項目があって、それが(asdf:operate 'asdf:load-op ...)にあたり、これを選択して、パッケージ名(この場合はCL-PPCRE)を入力してEnterを押せば良い。

もっとも、正直な話をすると、選択肢表示がSLIMEの機能だったのかどうか自信がない。@rubikitchさんがメインテナンスをしているanything.elの機能だったかもしれない。色々入れすぎていて、既に何だか不明なのだ(笑)。

詳しくは@rubikitchさんに訊くか、あるいはこの辺を参考にして欲しい。既にanythingはEmacsの必須ツールとなっている。これ無しでは、Emacsでプログラミングなんてメンド臭くてやってられない、って程だ。

また、Emacs Wikiの方でSLIME用のanything-slime.elが公開されている。これも合わせて環境にインストールしておこう。

※3: 言うまでもないが、a、s、d、fは全て、QWERTY配列キーボードの上から3段目の左から順に並んでいるキーである。


What the FUCK caught ERROR?



まず最初に言っておくと、ASDF自体が新しい、って事です。かつこれは仕様範囲外なんですよね。そして、それをマトモに取り扱ってる書籍が無いです。



そこで、簡単な例を上げてみます。LOLの原著サイトの方で、本に書かれてあるコード(正確に言うと、若干改良したもの)がProduction Codeとして上げられています。考え方としては当然、




「う~ん、本を読みながらコード打ち込んで行くのもいいけど、取りあえず全コードを入手して、それを実際動かしながら調べていく、ってのもアリかな?」


と言うのはアリでしょう。アリですよね(笑)?あるいは、LOLの秘密兵器、defmacro!だけが必要で、他の御託は聞きたくない、とか(笑)。そう言う人も居るはずです(笑)。いてもおかしくない(笑)。



いずれにせよ、そう言ったシナリオを考えてみて、そこのコードをコピペしてlol-production-code.lispと言ったファイルを作ったとしましょう。そしてそれをコンパイル/ロードするとする(Lispbox/SLIMEだったらC-c C-kですか)。ハテサテ、これで上手く行く筈、と思ったら、何とエラー表示。次のようなエラーが表示されると思います。






「え?何でやねん?」


とか思う筈です。あせります。そもそも公式サイトから取ってきたコードなわけですから書き間違いでエラーが出る筈がない。ところが実際、コンパイルは失敗してエラーが出てる。おかしくないのか?



んで、ちょっと落ち着いてエラー表示を読んでみます。次のような事が書いてあります





defmacro!によるdlambdaの定義のマクロ展開時に於いて、エラーが発生。関数o!-symbol-pが定義されていない。



ここで「おかしいな?」となるでしょう。慌ててファイルlol-production-code.lispをチェックしてみる。Doug Hoyteの野郎、o!-symbol-pの定義書き忘れたんじゃねえの?……いや、でもファイル内をインクリメンタル検索してみるとo!-symbol-pはファイル内でキチンと定義されているのです。何じゃこりゃ、Common Lispがぶっ壊れている???



Macro-expansion before Compilation



実際、このテの一見意味不明なエラーは、原則インタプリタであるSchemeじゃお目にかかりません。Scheme慣れしていると皆目見当が付かない現象なんです。しかし、これはCLの仕様から言うとCL特有の、かつ当たり前の現象なんですよね。起こってるのは次のような事です。



CLの仕様によると、マクロはソースコードのコンパイル時に展開される事になっています。しかし、より正確に言うと、マクロは実際にソースコードをコンパイルする前に展開されるわけです。ここがポイント。何故なら、マクロはLispプログラミング上のコードのショートカットを目論んでいる機能ですから、マクロ定義に従って、マクロのソースコードをバンバン置換していかないとならない。つまり、それこそがマクロ展開であって、これが終了しない事にはファイル自体がコンパイル出来ないわけなんです。



と言う事は、マクロ展開中には、ファイルに定義されている関数が利用されてた場合、当然その関数はまだ存在してないって事なんです。上に紹介した現象に従うと、マクロdlambdaは同じファイル内に定義されているdefmacro!を用いて定義されてるんですが、そのdefmacro!は同じファイル内に存在しているo!-symbol-pに依存しています。しかしながら、マクロ展開中にはまだo!-symbol-pは存在していません。何故ならo!-symbol-p自体の評価もコンパイルもまだ行われてないから、です。そこで、Lispのコンパイラはこれを異常と検知し、コンパイルを中断してエラーを返しているのです。



いやはや、言われてみると「なるほどな」ってんで納得するでしょうけど、同時に何てメンドくせえんだとも思うでしょう(笑)。実際僕自身がハマってましたから(笑)。



そして、もっと言うと、Lispコンパイラはたまたま最初に見つけたo!-symbol-pが発見出来なかった、ってんで警告を発してコンパイルを中断したワケなんですけど、良く良く考えてみると、LOLと言う本の性質から考えて、あらゆるマクロ/関数はファイル内部で依存しあってるのは自明です。o!-symbol-pだけ最初に評価しとけば済む、って話じゃないわけです。考えただけでアタマがクラクラしてきますね(笑)。他にもまだ潜在的に色々な障害があるってのが予想出来ますから。



一体このマクロ展開とコンパイル自体のタイムラグはどうやって修正すんの?



Eval-When?



テキスト勉強中にしこしこREPLにコード打ち込んて動かしたり、あるいは、テキストエディタにLispコードを書いて部分的にS式を評価して実行する以上、上に書いたようなコンピレーションの問題は具現化しません。しかしそれじゃ面白くない。



ポール・グレアムがOn Lispと言う言葉で表現したかったのは、Lispでプログラムを書く、と言う事は裸のLispの上にレイヤーを構築していって、思い通りの言語体系を築き上げて、目的のアプリケーションを書くのに使う、と言う事です。と言う事は他の言語に比べてもフレームワーク作成の重要性が極めて高い、と言う事でもあり、何て事のない小さなプログラムを書いてもいずれ大きなアプリケーションの一部になる可能性が常にある、と言う事でもあります。



つまり、Lisp程コードの再利用が重要な言語もないわけです。かつ、コードを再利用する、と言う事はファイルに記述して保存しておかないとならない。しかし、単純にファイルに記述して保存した途端、上に挙げたようなコンパイル時のトラブルが待ち構えています。困ったもんだ。



もう一回上の例を鑑みて状況を整理すると、あるマクロ展開時にそのマクロに必要な関数が評価されていれば問題は起こらないと言う事です。つまり、恣意的に一部分の関数が先行評価されていれば良い。その方法は?と言うのがここでの議題なんです。



LOLはマクロ記述の為の指南書なんですが、残念ながらその手の方法に付いては全く記述していません。つまり、それなりにCommon Lispの知識を持ってるのが前提なんですが、ところが、先に書いたようにその割には最初の数章がロクにCLに付いて知識が無い人間を前提にしたような記述をしている。想定読者が意味不明だ、って言った理由が分かるでしょ(笑)?んで、この手のトラブル対処法を知ってる人なら当然マクロに付いても既に豊富な知識がある筈だって事を言ってたわけなんです。



さて、この問題に対処する為には暫定的には次の三つの対応策が考えられます。




  1. 常にREPL上で必要な関数をメンド臭くても先に評価しておく。

  2. eval-whenを使う。

  3. ASDFを用いてシステムとしてパッケージ化してしまう



テキストの例に上がっているコードをREPLでシコシコ評価して勉強している以上、1番の方策は当然アリです。が、考えただけでもメンド臭いですよね(笑)。じゃあ、何かのマクロを書いて、それに必要な関数は別なファイルに纏めておいて、先にCLにロードして評価してしまう、ってのもアリかもしれません。が、いずれにせよちとメンド臭い。後々の事を考えると当然ですよね。defmacro!を利用する為に別のファイルにわざわざまとめておいたg!-symbol-po!-symbol-pを先にロードする……まあ、やっても良いかもしれませんし、悪くは無いんですが、頻繁にそれ、ってのも困ります。特にコードの再利用を考えると手順はメンド臭くない方が良いわけです。



2番目のeval-whenを使う、と言うのがまさに直接的な回答かもしれません。まさしくこれが、恣意的に先行評価を起こさせる特殊オペレータだから、です。ところが、問題は…手元の資料を見る限り、解説されてないんですよね(笑)。使い方が(笑)。今までの流れを考えると、これだけ大事な特殊オペレータなんですが、ほぼシカトされています(笑)。Common Lispの本書く人は「Lispは実用に使える言語だ!」って前書き辺りで強調するケースが多いんですが、見てきた通り、ファイルに纏めると問題発生、しかもその解決策を書いてないんだったら絵に描いた餅だろう、とか思うんですけど(笑)。



唯一、eval-whenに付いてマトモにページを割いてるのは実践Common Lispくらいですね。この著者はさすが著書に「実践」って名づけるだけあって、アプリケーション作成に於いて何が問題になるのか良く知っています。抽象論にしていない。



原書サイトで解説書いてあるんで、読んでみても良いでしょう。一部日本語訳から抜粋してみます。今まで記述してきた問題点を端的に表現しています。




DEFMACROの展開形にはEVAL-WHENが含まれているので、そのファイル内でマクロを定義した直後から使うことができる。しかしマクロを定義しているファイルでマクロを使うには、マクロだけでなく、マクロが使っている関数もすべて定義されている必要がある。ところがDEFUNでは、通常はコンパイル時に関数が有効にならない。そこでマクロが使うヘルパー関数のDEFUNをすべて:compile-toplevel付きのEVAL-WHENで包むことにより、マクロの展開関数が走るときにその定義が使えるようになる。場合によっては:load-toplevel:executeを付けてもよいだろう。ファイルのコンパイルやロードの後、もしくはコンパイルする代わりにファイルをロードする場合は、関数の定義が必要になるからだ。
実践Common Lisp


ぶっちゃけ、僕も実践Common Lispは読んだ事は読んでたんですが、すっかり失念していました(笑)。プログラミングでは実際にトラブルに見舞われないと、重要な示唆がどれだけ重要か、って実感出来ないんですよね。困ったもんです。



なお、eval-whenはMacLisp由来だそうで、確かに同じくMacLispの直系の子孫であるEmacs Lispのファイルではeval-when-compileと言う記述を良く見かけます。Emacs Lispを書いている人にはひょっとしてお馴染みなのかもしれません。



Altanative 3: ASDF



さて、第3の選択肢で、ここで扱うのがASDFです。eval-when自体がロクな解説が無いんで嫌ってたんですけど、実はASDFに関しては輪をかけて解説が書いてある書籍が無いです。



じゃあ何でASDFなんだ?それ以前にASDFって一体何なの?端的に表現すると、ASDFとはCommon Lisp用のMakefileです。つまり、ファイル同士の依存関係をハッキリさせて順序良くコンパイルしてロードするように指定する仕組み、なんです。と言う事は、(asdf:operate 'asdf:load-op ...)と言うのは、言わば、GNUのツールで言うmakeなんです。



そして概念的にはMakefileだ、と言う事は、当然eval-whenが必要になるような場面でも、明示的にeval-whenを指定しなくても全て解決してくれる、と言う事です。非常に有難い仕組みなんですよ。使う分にはね。少なくともLinux系のOSでアプリケーションのインストールでmakeを使うよりゃメンド臭くない。



@valvallowさんがブログでOn Lisp と Let Over Lambda のコードって一挙に紹介してたんですけど、僕が何故これやらなかったのか、と言うと、前述の通り、ファイルのコンパイル/ロードで面倒な事になる、って知ってたから、です。と言うかこの記事が上がる前に既になってた(笑)。そして、何故Debian配布のOn Lispのコードにこだわってたのか、と言うのも、この手のコンパイル/ロードに関して言うと面倒が無いから、です。debヴァージョンはASDF化してる。使う分にはラクチンで殆ど何も考えなくて良いのがASDFと言うシステムなんです。



反面、自分でMakefileを作る、もとい、ASDFを設定する、ってのはメンド臭いです。何せ資料となる書籍が無い、んで。僕自身も過去何度か挑戦したんですが、失敗してメンド臭くなっちゃった(笑)。ASDFに付いて少しでも記述してるのがまたもや実践Common Lispだけ、と言う有り体なんですが、そこでもこんな事が書いてあります。





ASDファイルの他の例については、Webで入手できる本書のソースコードが参考になるはずだ。実践の各章で使ったコードは、適切な内部システム依存関係と一緒に、システムとしてASDファイルに記述されている。



実質これしか書いてないんです。事実上、ググレカスと言ってるわけですよ(笑)。とは言っても日本語で読めるASDFの解説なんて知りませんし、結局英語のマニュアル読まなアカンのか(笑)。うげえメンドくせえ。そもそもマニュアルなんて母国語でさえ読みたくねえシロモノなのに(笑)。そうだろ、皆の衆(笑)?



とは言っても、Lisp勉強していくうちに、そのうちCommon Lispで書かれたアプリケーションを配布したい、と言うような野望がある人もいる事でしょう。CLでのexecutablesの作り方、ってのは謎の部分が多いんですが(マジで多い)、それに比べればASDFはまだ敷居が低そうに見えます。なんせ所詮Makefileですし。しかも前項までの問題、要するにeval-whenにまつわる問題も解決してくれる。一石二鳥です。



てなわけで実践Common Lispの配布コードと首っ引きになってASDFの設定方法を分析していました。再度挑戦です。ここいらでASDFにカタ付けといても良いだろ、と言う個人的動機と、日本語で書かれたASDFの説明がほぼない、って辺りで良いイントロダクションになれば良いな、と言う二つの目的があります。まあ、専門家じゃないんで勘違いもあるかもしれませんが、そこはブログ記事なんで、適当に補完出来る人はより正確な記事を書いてみてください。あとは任せた(笑)。



ところで、ASDFに入る前にCLのパッケージと言われるシステムに付いて軽く解説しておきます。本当はやりたくねえんだけど(笑)、これ解説しとかないとワケワカメなんでしょーがない。



What the FUCK are Packages?



パッケージとは、端的に言うとCommon Lisp上での名前空間を分離/分割する仕組みです。そしてこの存在に絡んでCLerとSchemerがまた喧嘩してんですよね(笑)。んなもんどーでもいいだろ、とか第三者的には思うんですが(笑)。当然、CLにはパッケージがあるけどSchemeには無い(※1)。そしてCLerに言わせると、このパッケージと言う仕組みがCLの設計の根幹を握っているらしい。



余談ですが、ポール・グレアムは独自に新しいLisp方言Arcを設計しています。んで、このArcには名前空間を分割する為のパッケージ、って仕組みが入ってない模様です。んで、当然の如く、Arcを試した層から「Arcにはパッケージが無いの?」と言う質問を受けたらしい。ポール・グレアムはそれに付いてこんな感じで回答していました。




パッケージが必要になったらArcに組み入れようとは思っている。ただ、個人的にはパッケージが必要だ、って思った事は一度もないんだ。


まあ、前にも指摘したんですが、ポール・グレアムはCommon Lispに関しても、自分の好きな機能にはページを多く割く傾向があって、反面自分が好きじゃない機能に関してはページをそんなに割きません(笑)。ANSI Common Lisp読んでも、パッケージに関する解説はちょっとばっかし、です。あんま使ってないんでしょうね(笑)。同様に構造体は好きだけどCLOSはあんま好きじゃない、って事も分かります(笑)。この辺、色んな意味で実践Common Lispの著者、Peter Seibelと極めて対照的です。



さて、本題に戻ると。Common Lispはちょっとしたデータベースのような仕組みになっています。例えば次のようにREPLに入力する。





hogeと言うシンボルをREPLに入力するとHOGEと表示する。Schemeなんかを鑑みても、Lisp系の言語だと当たり前の反応なんですが、実はこの時点でCommon Lispでは背後でHOGEと言うシンボルをデータベースに登録しています。このデータベースはシンボル専用のデータベースで、平たく言うとこれがパッケージです。そして、ここで使われているパッケージがCL-USER(本当はCOMMON-LISP-USERと言う名称)のパッケージです。プロンプトに表示されているCL-USERと言うのはこのデータベース、もとい、パッケージ名を表しているんです。



つまり、REPLでシンボルを入力する度に新しいシンボルだったらCommon Lispはパッケージと呼ばれるシンボル用データベースにそのシンボルを登録していきます。これをCLではシンボルをパッケージにインターンすると言います。もし登録済みのシンボルだったら、当然その中身を調べるわけですよね。つまり変数なのか関数なのか、はたまた属性リストなのか、と。端的に言うとこれがCLがユーザーに見えない部分で行っている事で、Schemeに比べると遥に複雑な事をやってるわけです。



ついでに言うと、表面的にはSchemeで言うstring->symbolとCLのinternは結果だけ見ると似たようなモノに見えるんですが、実は意味が違うんです。Schemeのstring->symbolは字面が表している通り単に文字列をシンボルと言う型に変換してるんですが、CLのinternの目的と言うのは、もちろん文字列をシンボルに直すんですが、むしろ明示的にパッケージにシンボルを登録する事、なんです。





internは見た通り、多値関数です。返り値が二つある。最初の返り値は"HOGE"をシンボルに直した表現が表示されていますが、二つ目の返り値は:INTERNALとなっている。これはつまり、「CL-USERと言うパッケージには登録済みのシンボルだよ」と教えている。何故かと言うと、先ほどREPLでhogeと入力していますからね。新規にinternすると、この部分はnilと表示される筈です。



そしてここで分かる事が一つあります。現時点REPLではCL-USERと言うパッケージを対象にしていました。当然CL-USERだけが唯一無二のパッケージ、ってわけじゃあない。他にもパッケージが存在する(※2)し、また独自にパッケージを作る事さえ出来ます。このパッケージ作成の為のマクロをdefpackageと言います。



以降、比較的Common Lisp入門の例が分かりやすいんでそれに準じてみます。





上の例では、最初にdefpackageで東京パッケージを定義しています。本体部に(:use :common-lisp)と記述しているのは、CLの基本機能を提供しているCOMMON-LISPパッケージを利用しろ、と言う指定です。(CL-USERとはまた別物。ちなみに短縮形はCL。)これが無いと東京パッケージに入った途端にCommon Lispの全機能が使えなくなると言うようなおマヌケな事態に陥るので忘れないようにしましょう(笑)。言わば要請されたデフォです(笑)。



二つ目の(in-package :東京)CL-USERから東京パッケージに入っています。in-packageと言うのがパッケージ間を行き来する為のマクロです。ここが実行されるとプロンプトが東京に変更されたのが分かるでしょうか?Lispbox/Emacs + SLIMEは親切な事に現時点どのパッケージにいるのか表示してくれます。



そして、三つ目のREPLでの支店長の評価により、シンボル支店長は東京パッケージへとインターンされます。



続いて、次のようにしてみます。





殆ど同じなんですが、三つ目に注目してください。先ほどはシンボル支店長は東京パッケージにインターンされました。今度はシンボル支店長は大阪パッケージにインターンされています。これが何を意味してるか、と言うと東京の支店長と大阪の支店長は全く違うシンボルだと言う事です。全然別人だ、と言う事ですね(笑)。これを次のようにして確かめてみます。





東京の支店長と大阪の支店長が全くの別人、もとい、別のシンボルになってる、って事がお分かりでしょうか。



では東京パッケージから大阪の支店長を参照する事が出来るのでしょうか?出来ますが、原則そう言う場合はシンボルをエクスポートしないとなりません。その為には関数exportを用います。





実はASDF自体がパッケージで、asdf:operateとかasdf:load-opとか、やたら名称が長くまたコロンが多用されているのもこれらがexportされたシンボルだから、なんです。CL-USERと言うデフォルトのパッケージでASDFと言う別パッケージのシンボルを利用するにはああ言う記述方法を用いる必要があり、パッケージに於いてはコロンは「~パッケージの」と言う意味になってるわけです。



しかし、場合によっては、コロン多用による記述がメンド臭い場合があります。そう言う場合、次のようにして明示的にシンボルをインポートしたパッケージを定義出来ます。





また、あるパッケージの全機能を使いたい場合、次のようにしてパッケージを定義すれば良いです。





これでパッケージの使い方の基本は全部見ました。パッケージとは要するに名前保護の為のシステムであり、また、明示的にあるパッケージからシンボルをインポートしたりエクスポートしたり、と言う事が出来るのです。Schemeと比べると遥に複雑ですし、最初からこれを真っ正面から取り上げている本は、それこそCommon Lisp入門くらいしかない、んですが、これを煩わしいと感じるか否か、ってのも正直人に依るとは思います。しかしASDFを取り上げる以上、このシステムは避けて通れないのは事実、なんです。んで、ぶっちゃけた話、ポール・グレアムがANSI Common Lispを記述した1995年辺りでは、ASDFそのものが恐らく存在してなかったんでしょう。パッケージ自体の歴史は長いにも関わず、ASDFのせいで近年異様にパッケージの重要度が上がってきた、と思います。



最後に一つだけ。CLerとSchemerの諍いの大体の元凶はマクロに付いて、なんです。LOLでもSchemeの衛生的マクロが批判されていましたが、両者ともマクロの事になるとアツくなる。特に名前の衝突に付いての話になると両者とも譲らないのです(笑)。



ここはパッケージの話を書いてる筈なのに何でいきなりマクロ、しかも名前の衝突、なんて話が出てくるんだ?と思う人もいるでしょうが、実はCLerはこのパッケージと言う仕組みそのものがCLのマクロを支えている、と考えている。そして極論を言うと、Schemeはパッケージを持たない辺りがダメなんだ、と考えている。その部分だけちょっと説明しておきます。



最初にREPLでシンボルを入力するとパッケージにインターンされる、と言う話をしました。その途端そのシンボルはパッケージに保護されるわけです。つまり、どのシンボルでも何らかの形でどっかのパッケージにインターンされてるわけです。たった一つの例外を除いては。それがgensymで生成されるシンボルなんです。



つまり、原則的にどのシンボルでもどっかのパッケージにインターンされて、何らかの方法で参照出来る、と言う建前があるが故に、どのパッケージにもインターンされない、つまり参照出来ないシンボルが生成出来ると言うカラクリが成り立つわけですよ。これがなかなか上手い手を考えたもんだな、と(笑)。CLerの主張とは、こう言った上手い手無しにマクロが書けるか?とSchemeを非難してるわけですね(笑)。



LOLで書いてたgensymの説明、と言うのは、今までやってたパッケージの動作とシンボルのインターン、と言うコンセプトを掴めばより分かりやすいのではないか、と思います。抜粋してみます。




Common Lispでは、シンボル(名前)はパッケージに結び付けられる。パッケージはシンボルの集合であり、与えられた文字列、つまりそのシンボルのsymbol-name文字列を使って、パッケージからシンボルを指すポインタが得られる。このポインタ(通常単にシンボルと呼ばれる)の最も重要な性質は、同じsymbol-nameでそのパッケージから探索された他の全ポインタ(シンボル)との比較が、eqで行われることである。gensymはいかなるパッケージにも属さないシンボルであり、そのシンボルとeqになるシンボルを得られるsymbol-nameは存在しない。名前を付ける必要なしに、1つの式の中で、あるシンボルが他のシンボルとeqとなるようLispに指示したい場合、gensymを使う。プログラマが名前付けを一切行わないため、名前の衝突は発生し得ない。



※1: もちろん、処理系によってはパッケージを持ってるSchemeもあります。中でも印象に残ってるのはScheme48と言う処理系です。CLばりのパッケージを持っていて、何とSRFIを使おうとしてもシンボルが保護されていてインポートしないと使えない、と言うのが強烈でした(笑)。

この辺のライブラリの扱いに対しても、処理系作成者の解釈/判断の余地が大きく、結局処理系間でポータブルなコードを書くのが難しくなる、と言うのがSchemeの特徴です。

    移植性のあるSchemeのコードを書くのはCommon Lispで書くのより骨が折れる。



※2: ではデフォルトでは一体いくつくらいパッケージがあるのでしょう?CLHSによると、仕様で標準として最低限要求されているパッケージは次の三つです。

  • COMMON-LISP: Common Lispの中核機能が定義されたパッケージ

  • COMMON-LISP-USER: デフォルトでユーザーと対話するパッケージ

  • KEYWORD: キーワード(アタマにコロンが付いてるシンボル)がインターンされるパッケージ


この3つが最低限要求されているパッケージなんですが、逆に言うと、これさえ満たしていれば、実装次第でもっとパッケージを追加しても良い、と言う事です。つまり、CL処理系の背後では様々なパッケージが動いている、と言う事になります。

処理系で使われている全てのパッケージを一覧するにはlist-all-packagesと言う関数を用います。これは今現在使用されている全てのパッケージ名をリストにして返します。

なお、SBCLの場合ですが、デフォルトの状態で次のようにREPLに入力

(loop for i in (list-all-packages) counting i)

してみると、49と言う数値を返してきます。つまり、SBCLのREPLの裏では49個ものパッケージが連帯しながらCL-USERパッケージを通じてユーザーと対話しているのです。


BASIC OF ASDF



と言うわけで、前項のパッケージの基本を踏まえてASDFの作成方法を記述していきます。ここでは、Lispプログラムとしてはマジでクダラないコードを扱う事にします。それこそ、Hello, World!って表示される程度でいい、と。その方がASDFの記述方法に集中出来るってなもんです。つまり、




(defun hello-world ()
(princ "Hello, World!"))


を対象のコードとします。



ASDFのファイル構成は次の三つが基本、です。




  1. packages.lisp

  2. プログラム本体のlispファイル

  3. asdファイル



これら三つのファイルがシステム定義を構成していて、これらの外枠のディレクトリ(あるいはフォルダ)内に存在すればいいわけです。ここではHello, World!と表示されるだけのプログラムとも言えないプログラムを用いてるんで、単純にhelloディレクトリをHOMEディレクトリ内に作る形とします。

まずはpackages.lispから。これは前項見た通り、プログラムが所属する(正確に言うと、プログラムが用いているシンボルがインターンされる)パッケージを定義します。雛形は次のような形です。





第一行目は、取りあえずこのパッケージ定義がどのパッケージ内で読まれるのか指定しています。デフォルトでCL-USERで読まれればまあ問題が生じないんで、CL-USERに移動しておきます。三行目以降でプログラム本体で使われているシンボルがインターンするべきパッケージを定義します。ここでCOMMON-LISPパッケージをuseするのを忘れないようにしましょう。もう一回繰り返しますが、COMMON-LISPパッケージがCLの中核の機能を定義しているパッケージなんで、これがないとCLの全機能が使えません(笑)。あと、exportはお好きなように。この例のHello, World!を表示するだけのようなクダラないプログラムでは、本体定義であるhello-worldと言うシンボルをエクスポートします。





このパッケージ定義を受けて2番目の本体のプログラムは次のようになります。





第一行にシンボルをインターンさせたいパッケージに移動する旨を定義します。この場合は当然、明示的にpackages.lispで定義されたパッケージ(helloパッケージ)へと移動する、って事ですね。そこさえ書いておけばプログラム本体はフツーに書いて構わない。



なお、この例(hello-world.lisp)ではin-package指定の部分に#を含んでいますが何故かは知りません(笑)。単に実践Common Lispの公式サイトで配布しているソースコードを調べた際に含まれていたんで従ったまで、です(笑)。CL-PPCREも調べてみたんですが、そっちには付いてませんでしたね。もう一つ言うと、一応ASDFのマニュアルもザーっと眺めてみたんですが、特に何も書いてませんでした(笑)。だから、あってもなくても構わないんじゃねえのかな(笑)?良く分からんわ(笑)。



さて、この時点で一つ分かる事があります。それはhello-world.lisppackages.lispに依存していると言う事です。これは当然ですよね。hello-world.lispはあるパッケージへと移動する旨があるのに、最初にそのパッケージが生成されてなければ意味がないから、です。逆に言うと、最初にパッケージが定義されてからプログラム本体が読み込まれないとならない。この手の依存関係を指定するのがasdファイルです。



asdファイルの雛形は次の通り。





一行目でシステムとしてのパッケージ名を定義します。これは先ほどpackages.lispで定義したパッケージとはまた別です。が、簡便性を優先して、packages.lispで作成したパッケージ名に-systemでも付けた形にしておけば良いでしょう。packages.lisphelloと言うパッケージを定義してたらasdではhello-systemと言うように。



もう一つ重要なのは、このパッケージではclパッケージ(つまりCOMMON-LISPパッケージ)をuseするのは当然として、ついでにasdfパッケージもuseする事を指定します。これは当然、ここではasdfパッケージで定義された全機能を用いなければならないから、です。ついでに言うと、雛形では四行目以降でdefsystemと言う関数が用いられていますが、これはANSI仕様にはない関数で、ASDFパッケージで定義されているもの、です。従って、これを使う以上ASDFで定義されてエクスポートされたシンボルを全てインポートしないとならない。



正しくシステムパッケージを定義しておいて、二行目でそのシステムパッケージに移動しています。ここはまあ、いいですね。



四行目以降からdefsystemを用いて、色んな情報(ファイルの依存情報や外部システムへの依存情報を含む)を記述していきます。システム名は先ほど同ファイル内に定義したシステムパッケージ名とは別です。原理的にはpackages.lispで定義したパッケージ名とも別です。そして、ここがASDFにシステム名として認識されます。お好きなキャッチーな名前を付けましょう(そして、それがasdファイルの名前になるでしょう)。ここではシンプルにhelloシステム、と名づけます。



以降、:name:long-descriptionはどーでもいいです(笑)。あっても無くても構いません。与えるものが文字列だ、って事さえ気をつければどう書いても構いません。



重要なのは:componentsです。ここでシステムに必要なファイル群とそれらの間の依存関係を指定します。つまり、今の場合はhelloディレクトリに含まれる3つのファイルのうち、packages.lisphello.lispの二つのファイルの依存関係がどうなってるのか銘記しないといけません。そしてどのみち、システムを構成し、プログラム自体が記述された全.lispファイルはpackages.lispに依存するだろう事は分かりきっています(何故なら、それがプログラム本体で使われる全シンボルのインターン先を定義してるから、です)。



また、:componentsで指定するファイルには特に拡張子は付けません。



それで、結果としてhello.asdは次のようになります。





これで完成、です。UNIX系OSだったら




ln -s hello/hello.asd ~/


とでもして、hello.asdのシンボリックリンクをHOMEディレクトリに作成します。Windowsだったらasdf:*central-registry*で示唆されているフォルダ内にhello.asdへのショートカットを作成すれば良いでしょう。そしてREPLで(asdf:operate 'asdf:load-op :hello)とすれば(※)、





と表示されてシステムhelloは無事コンパイル/ロードされます。use-packagehello-worldが使えるのか見てみましょう。





上手い具合に動いていますね。シンボルhello-worldはエクスポートされてるんで、use-packageすればCL-USER内でhello-worldを参照出来る、と言う事です。



以上がASDFの基本的な定義方法の紹介です。




※: 繰り返しますが、Lipbox/Emacs + SLIMEだったらREPLでカンマ(,)、load-system、Enter、hello、Enter、です。


":depends on" in :components of a System



さて、今まで見てきた通り、これがASDFの定義方法の全て、です。んで、蛇足になり兼ねないんですが、システム内に一つのasdファイル、一つのpackages.lispファイルはともかくとして、本体のプログラムは複数の*.lispファイルに分散される場合がある、と言う事は自明だと思います。当たり前ですよね。



単純に、複数の*.lispファイルがある場合は、asdファイルの:componentsの欄に拡張子を外したファイル名を列挙すれば良い、って事です。必ずpackages.lispに依存している事を明記して。ただ、問題はそれら本体のファイル同士が何らかの形で依存している場合、です。



例えば、またクダラない例ですけど、hello-worldと言うプログラムが次のような二つのファイルに分散されている例を考えてみます。







body-of-hello-world.lispではプログラムhello-worldが定義されていますが、本体内で大域変数*h*が参照されています。そしてその*h*はこのファイル内では定義されていません。*h*は別のファイルであるstring-of-hello-world.lispで定義されている。つまり、言い方を変えると、body-of-hello-world.lispstring-of-hello-world.lispに依存していると言う事です。



こう言う場合、asdファイルの:componentsの欄は次のように記述します。





:components:file:depends-onはリストを取り、そこには複数の依存先ファイルを列挙出来ます。複数のファイルに依存する場合は、馬鹿正直に複数の依存先ファイル名を列挙すればO.K.です。



またREPLでhelloシステムが上手く動いているのかどうか見てみましょう。





上手い具合に動いてます。かつ、この時点ではpackages.lispに特に変更を加えていません。つまり、シンボルhello-worldはエクスポートされていますが、一方、大域変数*h*はエクスポートされていません。従って、CL-USERhelloパッケージをuse-packageしても*h*は参照不可能です(※)。





もう一つ依存パターンを考えてみます。body-of-hello-world.lispには特に変更は加えませんが、string-of-hello-world.lispが別の二つのファイル、a.lispb.lispに依存しているもの、とします。つまり、次の3つのファイルがpackages.lispbody-of-hello-world.lisphello.asdと共にhelloディレクトリ内にある、とする。









a.lispは大域変数*a*を定義、b.lispは大域変数*b*を定義していて、これらの間には相互依存関係はありません。一方、string-of-hello-world.lispは大域変数*a**b*を結合している。つまり、string-of-hello-world.lispa.lispb.lispに依存してるわけです。そしてbody-of-hello-world.lispstring-of-hello-world.lispに依存してるんですけど、言い換えるとa.lispb.lisp間接的に依存しているわけです。



こう言う場合のasdはどうなるのか、と言うと、次のようになります。





ご覧のように、間接的に依存しているファイル名は明示しなくて構いません。あくまでpackages.lispのように、直接的に依存しているファイル以外は無視して結構です。従って、string-of-hello-world.lispの依存関係が解消された時点で、body-of-hello-world.lispstring-of-hello-world.lispだけに依存している、と言うわけです。




※: 嘘です。ホントは無理矢理参照可能です。ただし、パッケージ、と言う名前保護のシステムの目的を考えると「エクスポートされていないシンボルを無理矢理参照する」と言うのは望ましくありませんし、実際、非推奨になっています。参照可能なシンボルは常にエクスポートされてる筈だ、と言う事で、ここでは「無理矢理参照する」方法は明記しません。


:depends-on the Other Systems



さて、今度はHOMEディレクトリにprint-name-of-functionと言うディレクトリを作成してみます。そこに次の三つのファイルを置いてみます。









前項までで見た通り、packages.lispではp-n-fと言う名前のパッケージを定義します。エクスポートするシンボルはprint-name-of-function.lispで使われるシンボル、print-name-of-functionです。



print-name-of-function.lispでは、関数を生成するマクロprint-name-of-functionを定義しています。このマクロは凄くクダラないんで、あんま解説したくないんですが(笑)、要するに適当な文字列を受けとると、




  1. 文字列を全部大文字に変換する。

  2. スペースと大文字のアルファベット以外を全て削除する。

  3. スペースをハイフンに変換してこれを関数名としてインターンして、元々与えられた文字列を出力する関数を定義する。



だけです。クダラないんで、まあいいでしょう(笑)。これはこれとして(笑)。



p-n-f.asdもまあいいでしょう。基本的な設定方法にしか従ってません。またもや、UNIX系OSだったら、このp-n-f.asdのシンボリックリンクをHOMEディレクトリに張り(Windowsだったらasdf:*central-registry*で指定されたフォルダにショートカットを作り)、p-n-fシステムをREPLに読み込んでみます。





上手い具合に動いているようですね。ご覧になった通り、p-n-fパッケージにインターンされているシンボルを持つマクロprint-name-of-functionは受け取った文字列(この場合は"Hello, World!")を関数名に相応しいように修正し、それを今いるCL-USERパッケージ(「今いる」パッケージをカレント・パッケージと言います)にインターンして関数hello-worldを自動生成します。



もっとも、"Hello, World!"を印字する関数を作る為だけのマクロとしてはコード量がクソ多いんですけどね(笑)。全くしょーもない(笑)。でもこんな事も出来るわけです。





くだんねえ(爆)。あんまりにもクダんないんで涙が出てきた(笑)。



ま、いっか(笑)。何故こんなクダラないマクロを作ったのか、と言うと、このprint-name-of-functionが定義されたp-n-fシステムをどうやってhelloシステムから呼び出すか、と言うネタが以降のネタなのです。要するに、システム同士の依存ってのが次のテーマです。



まず、helloディレクトリ内の改訂版packages.lispは次のようになります。





ここはまあいいですよね。ずーっと上の方にも書きましたが、ここでCOMMON-LISP以外にも必要になるパッケージがあったらそれも合わせてuseするって事です。今はp-n-fパッケージ(システムではない)が要り用になるのが前提なんで、p-n-fパッケージも指定しておきます。



次はプログラム本体部のコードです。今回はp-n-fパッケージに含まれているprint-name-of-functionマクロを使うのが前提なんで、ファイルhello-world.lispは次のようになっています。





簡単に定義出来ますが、もう一回次の二点を確認しておいてください。




  1. packages.lisphelloパッケージを定義している。そのパッケージは外部パッケージp-n-fからエクスポートしている全シンボルをuseしてるのが前提である。

  2. hello.lispは一行目でhelloパッケージ内に移動する事を指定している。helloパッケージはp-n-fがエクスポートしている全シンボルを共有しているので、p-n-fパッケージで定義されているprint-name-of-functionマクロを使用可能である。



この二点が前提の為、ここでprint-name-of-functionを利用して関数hello-worldを定義出来るわけです。



最後にhello.asdです。それはこう言う風になります。





注釈を付けておきましたが、hello-systemと言うパッケージ自体がp-n-fパッケージに依存しているわけじゃありません。システム定義を良く考えてみたら分かりますけど、ここのパッケージはhelloパッケージにさえも依存していない、のです。あくまで、プログラムとしてのシステム全体を操作してるわけじゃなくって、単にファイルの読み込み順序や必要になる外部パッケージを指定してるのが、このシステムパッケージの役目だ、と言う事を覚えておいてください。



そして、必要になる外部パッケージは:components内で指定するのではなく、それとは別に:depends-onで指定します。ここで指定されてるのは外部システムそのものではなく、外部システム内に存在するasdファイルです。つまり、システムを纏めてあるディレクトリ自体を指定してるわけじゃあない、って事です。また、パス指定なんて高度な事をやってるわけでもありません。従って、要り用になる外部システムのasdファイルもasdf:*central-registry*にシンボリックリンクが張られている必要があります。この例だと、hello.asdのシンボリックリンクもp-n-f.asdのシンボリックリンクも(UNIX系OSでは)HOMEディレクトリ内に存在していないといけません。ASDFがhello.asdを呼び出した時、そこに書かれている定義に従って、asdf:*central-registry*内で、p-n-f.asdを探します。見つかったら、今度そこに書かれてある定義に従って、p-n-fパッケージをコンパイル/ロードします。見つからなかったらエラーを返す、と言う算段です(※)。



これで、(asdf:operate 'asdf:load-op :hello)したら、依存したシステムも合わせてコンパイル/ローディングされてCLに読み込まれます。





先ほど、直接REPLでprint-name-of-functionを使ってみた時と違い、定義された関数のシンボルhello-worldはパッケージhelloにインターンされている事に注目してください。関数internはカレントパッケージへとシンボルをインターンします。マクロprint-name-of-functionで関数hello-worldを生成したのはhelloパッケージ内、でした。従って関数生成時点では、カレントパッケージであるhelloへとシンボルhello-worldがインターンされたわけです。




※: ぶっちゃけ、Common LispでもSchemeでもパスの概念が丸っきりないんじゃないかって思う。歴史的に言うと、多分その通りで、そもそもこの二つはUNIX前提で生まれたわけではない。当然、ディレクトリ・ツリーなんて言うアイディアも元々UNIXのものなんで、そう言う概念には縛られてないのだろう。

特にCommon Lispの場合、そもそもこの規格がLisp OSのサブセットにあたる、と言う話である。かつ、「どのOSのシステムとも迎合化しない汎用のシステム」を目指したらしい。ワケ分かんね(笑)。

従って、良く分からないシステムを相手にする場合、この手のファイルのパス指定は、OS側に素直に任せておいて(例えばバッチスクリプト/シェルスクリプトを書く、シンボリックリンクを張る、とか)、CLやScheme内で解決しようとしない方が得策な感じがする。Rubyだったらこうはならねえんだろうな(笑)。

この辺に関しての話も、実践Common Lispの第14章第15章辺りに記述が成されている。興味のある人はご一読を。

なお、ポール・グレアムがArcを作ろうと思った理由の一つは、現存主流OSとのこの手の相性の悪さにイライラしたから、らしい。また、ポール・グレアムはLispは好きだけどLisp OS嫌いで、Arc製作の段では「UNIXが勝った!」と気を吐いていた。いずれにせよ、Arcの目的の一つは、Perl/Rubyのように「UNIX系OSに密着した」Lispを作りたかった、と言う事らしい。


Well, that's almost all



まあ、これでASDFの殆ど全て、だと思います。知ってる範囲内では、と言う事ですけど。いずれにせよ、LOLやあるいはOn Lispを読んで勉強していきながら、どんどんASDFとして纏めて行った方がコードの再利用性を考えると得策だろう、と思います。ずーっとREPL開きっぱなしにしてるわけにも行かないしね(笑)。



最初の方にも書きましたが、CLでのexecutableの作り方、ってのは依然謎が多いです。個人的にはまだ良く分かっていません。経験上、executableを実験的に作って成功したのは、PLT Schemeのみ、と言う有様です(それでも謎が多いんですけど)。しかし、ソースファイルを含んだディレクトリをtarball(あるいはzip)に落としてアプリケーションを配布する、って夢に関して言えば、ASDFを用いればかなり近づける、とは思います。これは武器ですね。



最後に。アプリケーションを書く際、ディレクトリの中にサブディレクトリを配置して、それぞれをASDFに纏めるとする。でも全体で一つのアプリケーションとして動かしたい場合どうするか?その場合、トップディレクトリにアプリケーション名を冠したasdファイルを置いておけば暫定的に解決は出来るだろ、って事だけは言っときます。asdファイルは別のasdファイルを呼ぶことが出来るってのがヒントです。:componentとして、って事ですけれども。重要なのは全てのasdファイルがasdf:*central-registry*にシンボリックリンクを張っている、って事だけです。

2010年5月1日土曜日

LOL SEGMENT-READER

第3章。リードマクロよりSEGMENT-READER。
これも書くんなら、末尾再帰の方がエエんちゃうの?と思ったケース。



Doug Hoyte氏は「効率」を考えてdoを使ってんのかな?と思わせておいて、いきなり普通に再帰する、と言うワケの分からん事をする。この人のスタイルは、ぶっちゃけ支離滅裂なんだよな(苦笑)。
例えば、まあ、冗談として聞いて欲しいんですが、マジメに効率考えてdoを使え、ってのなら徹底して次のようにして書く事も可能なんです。



冗談ですけどね(笑)。こんなコード読むの大変ですし。書くのも大変。ただ、分かって欲しいのは、そもそもdoの性質からしてletが要らないだろって事です。letが要らなければ本体部も要らない、っつー事です。

LOL SHARP-DOUBLE-QUOTE と SHARP-GREATER-THAN

実験も兼ねて。

valvallowさんを真似て、github使ってみようかな、と。
まあ、単に今のままでは、ブログにコード貼り付けると、<pre>タグ使っててもインデントがズレて嫌なわけですよ。どうにかなんねえのかな、とか思ってて。
んで、valvallowさんがgist使ってうまい具合にやってるんで、それを真似してみよう、って思ったわけです。で、まあ、gitそのものは要らなかったよね(笑)。gistとgitって違うみてえ(笑)。レポジトリ、なんて作らんで良かった、って話なんですが(笑)。単にアカウント取れば良かっただけの話、と言うオチ(笑)。

それはさておき。LOLをvalvallowさんに続いて読んでるわけですが。他の人の意見はともかくとして読みづれえ
いや、地の文体がどーの、って話じゃないです。単にコードが読みづらいって話なんだよな(苦笑)。精読試みると引っかかっていけねえや。
第3章、リードマクロからもうこれが破壊的操作の嵐でさ(苦笑)。こんなんここまでやる必要あんのか?とかぶっちゃけ思ってしまいました。もちろん、効率性考えれば必要になるケースってのがあるんですが、僕が思うトコ、この著者って美的観点ってのが全くねえんじゃねえの、とか思ってるのです(笑)。不遜ですけどね。
いやね、実際問題。無頓着にコード書いてるようにしか見えないし、心情的にはSchemerの筈のポール・グレアムも「こりゃあねえだろ」と思うんじゃねえのかな、と(笑)。そこまでいかんでも、ここに列挙されてるコードを他の言語のユーザーが見たら

「不必要に括弧が多すぎ!これだからLisperはよお。」

とか思うんじゃねえのか、と(笑)。ケンケンガクガクですよ(笑)。マジな話でさ。

リードマクロって考え方自体は単純ですよね。単に関数書いてやって、それをset-dispatch-macro-characterで文字指定してやってその関数と関連付けちゃえばおしまい、です。普通にマクロ書くよりラクかもしれませんね。set-dispatch-macro-characterって長ったらしい名前もEmacs + SLIMEだったらEsc-Tabで補完しちゃえばラクラク記述可能です。Viva! Emacs!
問題はその関数の書き方だ。個人的には川合史朗さんが何でも再帰で記述してたような「ローカル関数を設計」してやった方が見た目スッキリすんじゃねえの?とか思うんですけどねえ。生粋のCLerだと違うのかな。doは副作用使う時は確かに便利なんですけど、このケースじゃそんなの多用する必然性がそんなねえんじゃねえの、って思いました。



まず最初。LOLのSHARP-DOUBLE-QUOTEを書き直したものです。多分Schemeやってる人はこっちの方が見やすいのでは、と思います。オリジナルのコードは破壊的操作しまくりですが、そのテの操作は一切止めています(笑)。
そもそも、Doug Hoyteって人はdefunの「暗黙のprogn」アテにし過ぎだろ、って気もしますしね。これは本読むと、結局欲しいのは(coerce (nreverse chars) 'string)なんですけど、これは単にdoの返り値にした方がいいんじゃねえの、って謎がまずあって。要するにオリジナルのコードはcharを破壊的にdoで弄くっていって、それはそれでほっといて、最後に(coerce (nreverse chars) 'string)なわけですよ。何じゃそりゃ、とか思って(笑)。Lispがどうの、って以前にそれじゃあ手続き型言語のfor文の書き方だろ(笑)。
一方、ignoreCLHS見ても良く分かんなかったんで、そのまま挿入しています。ロジック考えると必ずしも必要である、とは思えないんですけどね。多分。ま、いいや、その辺はCLの流儀、と言う事で。



ってな感じで「Schemeっぽく」書いても問題なく動きますね。では次。



これも原版のコード見ても何が何だか……(苦笑)。いやあ、凹みましたよ(笑)。これもDoug Hoyte氏の手癖か何だか知らないんですが、やっぱりdefunの暗黙のprogn頼みのコードで。かつ破壊的操作をしまくりなんで、何が目的でどう操作してるのか、流れが掴み辛い。あんなにインデントが深くなる必要って全然ない、と思うんですけど。これも再帰に書き換えて、引数内処理した方がスッキリ決まるんじゃないか、って思いました。
暗黙のprogn頼みが何なのか、と言うと。要するに、データをループ回して操作して。それを置き去りにしてまた別にデータ持ってきてループ回してるわけです。それを逐次処理するってのがオリジナルのコードの狙いなわけですが。必要か、それ?とか思ってさ(笑)。
しかも脱出条件が良く分からん。何で(null pointer)が二ヶ所に渡って分散してんだか、皆目見当が付かなかった(笑)。「何じゃこりゃ?」ってのが正直なトコで。破壊的操作を無自覚に使ってるからそーなるんだとしか思えなかった(笑)。
そこで、上の関数ではローカル関数foobarを二つ作って凌いでいます。根本的に、この二つの関数って「独立」で構わないんですよ。オリジナルのコードでも最初のループって、結局一種のフラグ作りなんですよね。本当にやりたい事は後者のループが握ってるんです。だから、シンプルにfooでフラグを作って、barで本操作をする、って設計にしました。やりようによってはもっとシンプルに書けるやもしれません。