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に認識されないと思います。認識させるには再起動かける必要があるのではないでしょうか。