いや、最初は色々と
「LispによるLispの実装?んな、言語設計者になりたいわけでもねえのにメンド臭い。っつーかさ。それってハッキリ言うとComputer Scienceの極めてTrivialなネタなんじゃねえの?やってられっか。Computer Science学科に在籍した事もねえのに。バカヤロー。」
的な文句言ってたわけなんですが。気づいたらすっかりハマってました(爆)。いや、意外と面白いんだ、これが(笑)。
今回は2回目のLisp実装への挑戦、って事で日曜日辺りからやりはじめたのかな?今回参考にした書籍は対話によるCommon Lisp入門 POD版という本。最近、#9LISP用にLOLにかかりっきりで、それはそれで楽しいんですが、週末時間が足りなくってストレスが溜まってた。そこで気分転換にとか思ったんですが、色んな意味でドツボにハマってました(爆)。
前回のSmall-Lispの実装を通じて、
- クロージャの実装ってどうやんの?
とか言う疑問があって。その点対話によるCommon Lisp入門 POD版
まあ、その失敗の理由も色々とあるわけですけれども。そもそも対話による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が前提で書かれたテキストなんで、この辺、上手いテが思いつきませんでした。っつーか先程書いた通り、データ構造設計が決まれば自ずとプログラムが決まるなら、このヒエラルキーが決定された以上、逃げようがないんですけどね。いや、参った参った。
この実装だと中身は循環構造でいっぱい
んで破壊的操作を目論むと当然中には循環構造なんかが自ずとから出てくるわけですよ(笑)。代表的なのはシンボルnil
。nil
のsymbol-value
はnil
なんですよね(笑)。これで如何にも簡単に循環構造の出来上がり、です。破壊的操作様々っつーか(笑)。ポインタが自分自身を指している(笑)。
あと、パッケージもひでえな。パッケージに登録された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のプログラミングの中でも特別な手法であり、固有の目的と問題がある。コンパイラに渡る内容に変更を加えられるというのは、コンパイラを書き換えられるのとほとんど一緒である。したがって、マクロを書くときには、言語設計者のように考えながら始めなければならない。
ぶっちゃけ、
「何この人言ってんの?」
とか思ったわけですけど(笑)。メタプログラミングやるのにあたって、プログラマが言語設計者のように考えなければならない?言語設計した事もねえのにか(笑)。だったら無理だろ、と(笑)。
いや、その通りなんだと思います。プログラマにメタプログラミングは通常要らないんですよ。歌うたった事ないヤツに「歌手のように考えろ」とか、演技した事無いヤツに「俳優のように考えろ」ってのは土台無理な相談でしょ。プログラムした事ないヤツに「プログラマのように考えろ」ってのはメタファとしては成り立ちますが現実的な提案じゃありません。しかしマクロは現実に存在してて現実に関連しているわけですから、別な言い方するとやっぱり「言語設計した事が無いヤツが言語設計者のように考える」ってのは無理なんです。んな事ありえない。
という事はだ。この提案には次の二つの意味が考えられます。
- 実はマクロ自体は言語設計者に取って一番便利な機能であって、ある意味プログラマの為のものではない。
- マクロを理解するには実際にプログラミング言語を実装してみる必要がある。
この二つですよね。そして、これらはLispに於いてはかなり接近している、って意味なんです。つまり、Lispの力がマクロにあり、他の言語に比べるとLispのLispによる実装が簡単だとしたら……。試してみる価値がありますし、恐らく自分でLisp実装をしてみるのがマクロを理解する早道でしょう。そして実装した途端マクロが絶対欲しくなる。コア機能を定義した後のライブラリ的な意味じゃない「機能拡張」はマクロがあったほうが俄然ラクだから、です。
多分、この辺が、Lispのマクロに関しての他言語ユーザーの誤解の源なのかな、とはちょっと思いますね。その辺の言語でその言語自体を実装する事自体が凄く難しい。Lispは機能拡張可能な言語ですが、それは言語設計との境界線を曖昧にしている。それが故にLispに於ける「メタプログラミング」の重要性がピンと来なくなるんで、マクロ是非論、ってのが出てくるのかも。
もう一回言うと、マクロは言語設計者の為の機能でプログラマの為の機能ではない事は明らかです。ただし、Lispに於いてはプログラマと言語設計者の境界線は曖昧だって事です。という事は曲りなりにもLispを実装してみる価値はある。
多分プログラマ的観点でいてもつまるところマクロって理解出来ないような気がします。何となくですけど。で、Lispに於ける境界線が曖昧だったら跨いでもエエんちゃうの?ってのを最近感じ始めました。向こうの水は甘いぞ(笑)。
まあ、残念ながら、今のトコ、defmacro
のプリミティヴとしての実装方法って分からないんですけどね。もうちょっとLisp実装を何回かやってみて、改めてArcのソースでも精読してみたいと思います。何かヒントがあるかも。
とまあ、これらが徒然と今回感じたもの、です。CLOSでのコードには腹立ちながらやってたんですが(笑)、曲りなりにもパッケージシステムの実装を通じて、Lisp-1でLisp-2が実装出来たんだ、ってのは感慨深いです。ちょっとレアケースかも、とか思っています。まあ、まだ色々と抜けもありますが、いずれにせよ、前回のSmall-Lispに比べても高機能になったんで良かったですね。苦労しただけの事はありました。
#!/usr/bin/env bash | |
mzscheme blisp.ss |
;; mzscheme blisp.ss | |
#lang scheme | |
(require "bang-lisp-ver.1.0.ss") | |
(!lisp-load "blisp.blisp") | |
(!lisp) |
;; bang-lisp is a subset of Common Lisp implementation, | |
;; based on the textbook, "対話によるCommon Lisp入門", | |
;; published from 森北出版株式会社. | |
;; # ISBN-10: 4627836090 | |
;; # ISBN-13: 978-4627836099 | |
;; Additionally, some library functions are imported from | |
;; Appendix B of Paul Graham's "ANSI Common Lisp". | |
;; # ISBN-10: 4894714337 | |
;; # ISBN-13: 978-4894714335 | |
;; Originally, "対話によるCommon Lisp入門" suggests to use | |
;; CLOS, Common Lisp Object System, to implement the subset | |
;; of Common Lisp; however, I do not like Object Oriented | |
;; Programming style; in fact, I do try that by using PLT | |
;; Scheme's define-struct. Therefore, this implementation | |
;; heavily depends on PLT Scheme in order to convert some OOP | |
;; idea to structures with some fun and ,also, headache. | |
;; Thanks to PLT to provide such a great Scheme implementation, | |
;; anyway. | |
;; People may see the topic like Scheme implemented on Scheme or | |
;; CL implemented on CL; however I think it must be rarely to see | |
;; CL implemented on Scheme and vice versa. Thus, even though this | |
;; is my personal lesson to understand Lisps more, it may be helpful | |
;; for the people, studying to implement a Common Lisp subset on | |
;; Scheme. It must be fun across Lisp-1 and Lisp-2, with PLT. | |
;; By the way, most of ! mark on this source does not related to | |
;; any destructive operations(some are yes, of course). | |
;; The way of naming procedures here just relies on the text book, | |
;; "対話によるCommon Lisp入門". | |
(module bang-lisp-ver.1.0 scheme | |
(provide (all-defined-out)) | |
;;; !Lisp 基本設定 | |
;; id の設定 | |
(define *new-id* 0) | |
(define (new-id) (set! *new-id* (+ 1 *new-id*)) *new-id*) | |
;; オブジェクトの生成 | |
(define-struct !object (id)) | |
;; 二つのオブジェクトの同値性判定 | |
(define (!eq? x y) (= (!object-id x) (!object-id y))) | |
;;; 構造体によるデータ抽象 | |
;; 構造体によるアトムの定義 | |
(define-struct (!atom !object) () | |
) | |
;; 構造体によるコンスの定義 | |
(define-struct (!cons !object) | |
(first rest) | |
) | |
;; 構造体による数値の定義 | |
(define-struct (!number !atom) (number) | |
) | |
;; 構造体による文字列の定義 | |
(define-struct (!string !atom) (string) | |
) | |
;; 構造体によるシンボルの定義 | |
(define-struct (!symbol !atom) | |
(name value function plist package) | |
#:mutable | |
) | |
;; 構造体による NULL の定義 | |
(define-struct (!null !symbol) () | |
) | |
;;; 構造体によるパッケージの定義 | |
(define-struct (!package !atom) | |
(name hash) | |
) | |
;;; 構造体による関数の定義 | |
(define-struct (!function !atom) () | |
) | |
;; 構造体による特殊オペレータの定義 | |
(define-struct (!special !function) | |
(code) | |
) | |
;; 構造体による funcallable の定義 | |
(define-struct (!funcallable !function) () | |
) | |
;; 構造体によるプリミティヴの定義 | |
(define-struct (!primitive !funcallable) | |
(code) | |
) | |
;; 構造体によるクロージャの定義 | |
(define-struct (!closure !funcallable) | |
(body parameters environment) | |
) | |
;; make-instance で構造体によるデータ型の作成(ショートカット) | |
(define-syntax make-instance | |
(syntax-rules | |
(!object !atom !cons !number !string | |
!symbol !package !special !primitive !closure) | |
((_ !object) | |
(make-!object (new-id))) | |
((_ !atom) | |
(make-!atom (new-id))) | |
((_ !cons x y) | |
(make-!cons (new-id) x y)) | |
((_ !number number) | |
(make-!number (new-id) number)) | |
((_ !string string) | |
(make-!string (new-id) string)) | |
((_ !symbol name value function plist package) | |
(make-!symbol (new-id) | |
name value function plist package)) | |
((_ !package name hash) | |
(make-!package (new-id) | |
name hash)) | |
((_ !special code) | |
(make-!special (new-id) code)) | |
((_ !primitive code) | |
(make-!primitive (new-id) code)) | |
((_ !closure body parameters environment) | |
(make-!closure (new-id) body parameters environment)))) | |
;;; データ変換 | |
;; !Lisp のデータから PLT Scheme 上のデータへの変換 | |
(define (!CL->plt !obj) | |
(cond ((!cons? !obj) | |
(cons (!CL->plt (!cons-first !obj)) | |
(let ((tail (!cons-rest !obj))) | |
(if (!null? tail) | |
'() | |
(!CL->plt tail))))) | |
((!symbol? !obj) | |
(string->symbol (!CL->plt (!symbol-name !obj)))) | |
((!string? !obj) | |
(!string-string !obj)) | |
((!number? !obj) | |
(!number-number !obj)) | |
((!object? !obj) | |
!obj) | |
(else | |
#f))) | |
;; PLT Scheme のデータから !Lisp 上のデータへの変換 | |
(define (plt->!CL obj) | |
(cond ((pair? obj) | |
(!cons! (plt->!CL (car obj)) (plt->!CL (cdr obj)))) | |
((null? obj) | |
!nil) | |
((symbol? obj) | |
(!intern! (plt->!CL (string-upcase (symbol->string obj))))) | |
((string? obj) | |
(make-instance !string obj)) | |
((number? obj) | |
(make-instance !number obj)) | |
(else | |
#f))) | |
;;; 手続き | |
;; !make-symbol! の定義 | |
(define (!make-symbol! !str !pac) | |
(make-instance !symbol !str #f #f #f !pac)) | |
;; null!? の定義 | |
(define (null!? !obj) | |
(!eq? !obj !nil)) | |
;; !intern! の定義 | |
(define (!intern! !str (!pac !*package*)) | |
(let ((str (!string-string !str)) | |
(hash (!package-hash !pac))) | |
(let ((!sym (hash-ref hash str #f))) | |
(or !sym | |
(let ((!symbol (!make-symbol! !str !pac))) | |
(hash-set! hash str !symbol) | |
!symbol))))) | |
;; コンスの定義 | |
(define (!cons! x y) | |
(make-instance !cons x y)) | |
;; 引数を評価しない手続き | |
(define (noeval-args! !args) | |
(let loop ((!args !args) (acc '())) | |
(if (null!? !args) | |
(reverse acc) | |
(loop (!cons-rest !args) | |
(cons (!cons-first !args) acc))))) | |
;; 引数を評価する手続き | |
(define (eval-args! !args local) | |
(let loop ((!args !args) (acc '())) | |
(if (null!? !args) | |
(reverse acc) | |
(loop (!cons-rest !args) | |
(cons (!eval! (!cons-first !args) local) acc))))) | |
;; funcall の定義 | |
(define (!funcall! !obj . args) | |
(cond | |
((!closure? !obj) | |
(!eval! (!closure-body !obj) | |
(let ((h (!closure-environment !obj))) | |
(for-each (lambda (key val) | |
(hash-set! h key val)) | |
(!closure-parameters !obj) | |
args) | |
h))) | |
((!primitive? !obj) | |
(apply (!primitive-code !obj) args)) | |
(else | |
#f))) | |
;; function の定義 | |
(define (!function! local !obj) | |
(cond | |
((!cons? !obj) | |
(make-instance !closure | |
(!cons-first (!cons-rest (!cons-rest !obj))) | |
(noeval-args! (!cons-first (!cons-rest !obj))) | |
local)) | |
((!symbol? !obj) | |
(!symbol-function !obj)) | |
(else | |
#f))) | |
;; 外部ライブラリのロード用手続き | |
(define (!lisp-load filename) | |
(letrec ((!load | |
(lambda (p) | |
(let ((x (read p))) | |
(cond ((eof-object? x) | |
(close-input-port p)) | |
(else | |
(!eval! (plt->!CL x)) | |
(!load p))))))) | |
(call-with-input-file filename !load))) | |
;;; 大域変数 | |
;; 大域変数としてのパッケージの定義 | |
(define !*package* | |
(make-instance !package | |
(plt->!CL "!CL-USER") | |
(make-hash))) | |
;; 大域変数としての nil の定義 | |
(define !nil | |
(make-!null (new-id) | |
(plt->!CL "NIL") | |
#f | |
#f | |
#f | |
!*package*)) | |
(set-!symbol-value! !nil !nil) | |
(set-!symbol-plist! !nil !nil) | |
;; 大域変数としてのパッケージの定義 | |
(hash-set! (!package-hash !*package*) "NIL" !nil) | |
;; 大域変数としての t の定義 | |
(define !t (plt->!CL 't)) | |
(set-!symbol-value! !t !t) | |
;; 大域変数としての pi の定義 | |
(define !pi (plt->!CL 'pi)) | |
(set-!symbol-value! !pi (plt->!CL pi)) | |
;; 大域変数としての lambda の定義 | |
(define !lambda (plt->!CL 'lambda)) | |
;;; !Lisp 本体 | |
;; REPL | |
(define *version* "!Lisp Ver.1.0\n") | |
(define (!lisp) | |
(call/cc | |
(lambda (k) | |
(define (repl) | |
(print-prompt) (!print! (!eval! (!read! k))) (newline)) | |
(define (loop exp) | |
(loop (repl))) | |
(display *version*) | |
(loop (repl))))) | |
(define (print-prompt) | |
(display (string-append | |
(!CL->plt (!package-name !*package*)) | |
"> "))) | |
(define (!read! k) | |
(let ((exp (read))) | |
(if (and (list? exp) | |
(memq (car exp) | |
'(bye quit end exit))) | |
(k 'GOOD-BYE) | |
(plt->!CL exp)))) | |
(define (!print! !obj) | |
(write (!CL->plt !obj)) | |
!obj) | |
;; eval | |
(define (!eval! !obj (local (make-hasheq))) | |
(cond | |
((!cons? !obj) | |
(!apply! (!cons-first !obj) (!cons-rest !obj) local)) | |
((!symbol? !obj) | |
(let ((binding (hash-ref local !obj #f))) | |
(or binding | |
(!symbol-value !obj)))) | |
(else | |
!obj))) | |
;; apply | |
(define (!apply! !obj !args local) | |
(cond | |
((!funcallable? !obj) | |
(apply !funcall! | |
(cons !obj (eval-args! !args local)))) | |
((!special? !obj) | |
(apply (!special-code !obj) | |
(cons local | |
(noeval-args! !args)))) | |
((!symbol? !obj) | |
(!apply! (!symbol-function !obj) !args local)) | |
(else | |
#f))) | |
;;; 特殊オペレータの定義 | |
;; quote の定義 | |
(define (!quote! local !a1) !a1) | |
;; if の定義 | |
(define (!if! local !condition !then !else) | |
(if (null!? (!eval! !condition local)) | |
(!eval! !else local) | |
(!eval! !then local))) | |
;; setq の定義 | |
(define (!setq! local !sym !form) | |
(let ((!value (!eval! !form local)) | |
(binding (hash-ref local !sym #f))) | |
(cond (binding | |
(set! binding !value) | |
!value) | |
(else | |
(set-!symbol-value! !sym !value) | |
!value)))) | |
;; defun の定義 | |
(define (!defun! local !name !parameters !body) | |
(set-!symbol-function! !name | |
(make-instance !closure | |
!body | |
(noeval-args! !parameters) | |
local)) | |
!name) | |
;;; 基本関数の定義 | |
;; eq? の定義 | |
(define (!eq!? !x !y) | |
(if (!eq? !x !y) !t !nil)) | |
;; null? の定義 | |
(define (!null!? !x) (!eq!? !x !nil)) | |
;; atom? の定義 | |
(define (!atom!? !x) | |
(cond ((!atom? !x) !t) | |
((!null? !x) !t) | |
(else !nil))) | |
;; list? の定義 | |
(define (!list!? !x) | |
(cond ((!cons? !x) !t) | |
((!null? !x) !t) | |
(else !nil))) | |
;; + の定義 | |
(define (!+! . list-of-!numbers) | |
(plt->!CL (apply + (map !CL->plt list-of-!numbers)))) | |
;; - の定義 | |
(define (!-! . list-of-!numbers) | |
(and (pair? list-of-!numbers) | |
(plt->!CL (apply - (map !CL->plt list-of-!numbers))))) | |
;; * の定義 | |
(define (!*! . list-of-!numbers) | |
(plt->!CL (apply * (map !CL->plt list-of-!numbers)))) | |
;; / の定義 | |
(define (!/! . list-of-!numbers) | |
(and (pair? list-of-!numbers) | |
(plt->!CL (apply / (map !CL->plt list-of-!numbers))))) | |
;; eval の定義 | |
(define (!eval*! !form) (!eval! !form (make-hasheq))) | |
;; consp の定義 | |
(define (!cons!? !obj) (if (!cons? !obj) !t !nil)) | |
;; stringp の定義 | |
(define (!string!? !obj) (if (!string? !obj) !t !nil)) | |
;;; 各定義のインストール | |
;; quote のインストール | |
(set-!symbol-function! (plt->!CL 'quote) | |
(make-instance !special !quote!)) | |
;; if のインストール | |
(set-!symbol-function! (plt->!CL 'if) | |
(make-instance !special !if!)) | |
;; setq のインストール | |
(set-!symbol-function! (plt->!CL 'setq) | |
(make-instance !special !setq!)) | |
;; eq のインストール | |
(set-!symbol-function! (plt->!CL 'eq) | |
(make-instance !primitive !eq!?)) | |
;; null のインストール | |
(set-!symbol-function! (plt->!CL 'null) | |
(make-instance !primitive !null!?)) | |
;; atom のインストール | |
(set-!symbol-function! (plt->!CL 'atom) | |
(make-instance !primitive !atom!?)) | |
;; listp のインストール | |
(set-!symbol-function! (plt->!CL 'listp) | |
(make-instance !primitive !list!?)) | |
;; + のインストール | |
(set-!symbol-function! (plt->!CL '+) | |
(make-instance !primitive !+!)) | |
;; - のインストール | |
(set-!symbol-function! (plt->!CL '-) | |
(make-instance !primitive !-!)) | |
;; * のインストール | |
(set-!symbol-function! (plt->!CL '*) | |
(make-instance !primitive !*!)) | |
;; / のインストール | |
(set-!symbol-function! (plt->!CL '/) | |
(make-instance !primitive !/!)) | |
;; first のインストール | |
(set-!symbol-function! (plt->!CL 'first) | |
(make-instance !primitive !cons-first)) | |
;; rest のインストール | |
(set-!symbol-function! (plt->!CL 'rest) | |
(make-instance !primitive !cons-rest)) | |
;; cons のインストール | |
(set-!symbol-function! (plt->!CL 'cons) | |
(make-instance !primitive !cons!)) | |
;; read のインストール | |
(set-!symbol-function! (plt->!CL 'read) | |
(make-instance !primitive !read!)) | |
;; print のインストール | |
(set-!symbol-function! (plt->!CL 'print) | |
(make-instance !primitive !print!)) | |
;; symbol-name のインストール | |
(set-!symbol-function! (plt->!CL 'symbol-name) | |
(make-instance !primitive !symbol-name)) | |
;; symbol-value のインストール | |
(set-!symbol-function! (plt->!CL 'symbol-value) | |
(make-instance !primitive !symbol-value)) | |
;; symbol-function のインストール | |
(set-!symbol-function! (plt->!CL 'symbol-function) | |
(make-instance !primitive !symbol-function)) | |
;; symbol-plist のインストール | |
(set-!symbol-function! (plt->!CL 'symbol-plist) | |
(make-instance !primitive !symbol-plist)) | |
;; symbol-package のインストール | |
(set-!symbol-function! (plt->!CL 'symbol-package) | |
(make-instance !primitive !symbol-package)) | |
;; funcall のインストール | |
(set-!symbol-function! (plt->!CL 'funcall) | |
(make-instance !primitive !funcall!)) | |
;; function のインストール | |
(set-!symbol-function! (plt->!CL 'function) | |
(make-instance !special !function!)) | |
;; defun のインストール | |
(set-!symbol-function! (plt->!CL 'defun) | |
(make-instance !special !defun!)) | |
;; eval のインストール | |
(set-!symbol-function! (plt->!CL 'eval) | |
(make-instance !primitive !eval*!)) | |
;; consp のインストール | |
(set-!symbol-function! (plt->!CL 'consp) | |
(make-instance !primitive !cons!?)) | |
;; stringp のインストール | |
(set-!symbol-function! (plt->!CL 'stringp) | |
(make-instance !primitive !string!?)) | |
) |
(defun car (x) | |
(first x)) | |
(defun cadr (x) | |
(car (cdr x))) | |
(defun cddr (x) | |
(cdr (cdr x))) | |
(defun cdr (x) | |
(rest x)) | |
(defun copy-list-aux (x) | |
(if (atom x) | |
x | |
(cons (car x) | |
(copy-list-aux (cdr x))))) | |
(defun copy-list (lst) | |
(cons (car lst) (copy-list-aux (cdr lst)))) | |
(defun copy-tree (tr) | |
(if (atom tr) | |
tr | |
(cons (copy-tree (car tree)) | |
(copy-tree (cdr tree))))) | |
(defun identity (x) | |
x) | |
(defun mapcar (fn lst) | |
(if (null lst) | |
nil | |
(cons (funcall fun (car lst)) | |
(mapcar fun (cdr lst))))) | |
(defun not (x) | |
(eq x nil)) | |
(defun pair (lst) | |
(if (null lst) | |
nil | |
(cons (cons (car lst) (cadr lst)) | |
(pair (cddr lst))))) | |
(defun second (x) | |
(cadr x)) | |
(defun third (x) | |
(car (cdr (cdr x)))) | |
(defun 1+ (x) | |
(+ x 1)) | |
(defun 1- (x) | |
(- x 1)) |