2010年10月5日火曜日

((Pythonで) 書く (Lisp) インタプリタ)

Pythonで書くSchemeインタプリタです。

実際は、Schemeとは呼べないんですけどね(笑)。仕様満たしてませんし。かつ単なる写経です(爆)。
作者はPAIPの著者として有名なPeter Norvig。過去はLisperとして有名でしたが、現在はPythonistaとして有名な人です。

「Lisp以外の言語で」Lispを実装してみる、ってのは初めての経験で、結構面白いとは思いました。
ただ、Pythonの構文の細かいトコには明るくないんで、「読解」自体は上手く行ってるとは思いませんが(笑)。んで、やっぱOOPは嫌いです(笑)。

ちなみに、Python2.6.xだとだいぶ改良されてはいますが、やっぱエンコーディングはめんどっちいと思っています。

2010年7月20日火曜日

Goldbach's conjecture

A list of prime numbers

Compare the two methods of calculating Euler's totient function

Calculate Euler's totient function phi(m) (improved)

Determine the prime factors of a given positive integer (2)

Determine the prime factors of a given positive integer

Calculate Euler's totient function phi(m)

Determine whether two positive integer numbers are coprime

Determine the greatest common divisor of two positive integer numbers

Determine whether a given integer number is prime

2010年7月19日月曜日

Sorting a list of lists according to length of sublists

Group the elements of a set into disjoint subsets

Generate the combinations of K distinct objects chosen from the N elements of a list

Generate a random permutation of the elements of a list

Lotto: Draw N different random numbers from the set 1..M

Extract a given number of randomly selected elements from a list

Create a list containing all integers within a given range

Insert an element at a given position into a list

2010年7月15日木曜日

Remove the K'th element from a list

Rotate a list N places to the left

Extract a slice from a list

Split a list into two parts; the length of the first part is given

Drop every N'th element from a list

Replicate the elements of a list a given number of times

Duplicate the elements of a list

Run-length encoding of a list (direct solution)

Decode a run-length encoded list

Modified run-length encoding

Run-length encoding of a list

Pack consecutive duplicates of list elements into sublists

Eliminate consecutive duplicates of list elements

Flatten a nested list structure

Find out whether a list is a palindrome

Reverse a list

Find the number of elements of a list

Find the K'th element of a list

Find the last but one box of a list

Find the last box of a list

2010年6月10日木曜日

Script-Fu








2010年6月9日水曜日

プロダクション・システム





2010年6月8日火曜日

dot-emacs-example.el

Emacs Newbiesの為に.emacsの雛形を公開します。Emacs23以上対応です。

以前、UbuntuのEmacs22辺りまではUbuntu Japanese Team制作のdot.emacs.exampleが同梱されていたのですが、Emacs23からは完全UTF-8対応になった為か、無くなってしまいました。
そこで利便性を考え、オリジナル(GPLライセンス)のdot.emacs.exampleを改造したのが以下のdot-emacs-example.elです。

勝手に持って行って下さい(笑)。

2010年6月2日水曜日

vallog: syntax-rules: bind-variables

vallog: syntax-rules: bind-variables


「...」だと、こうは書けないですよね。。


多分こう書くのではないでしょうか。


オリジナルのコードだとsyntax-errorっての使ってますが、これはR5RSで定義されていないので、errorに差し替えています。
もっとも、2番目のパターンだとそれじゃあ怒られたので、syntax-errorの挙動がしりたいトコなんですけどねえ。

いずれにせよ、省略子で書いた方がパターン的にはシンプルなのではないでしょうか。

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で本操作をする、って設計にしました。やりようによってはもっとシンプルに書けるやもしれません。

2010年4月26日月曜日

#9LISP 014 メモ

#9LISP はいよいよマクロを通してCommon Lispに突入するそうです。ぶっちゃけ「良かったな」と(笑)。


@valvallowさんと、


「LOLを参考にして、Forth実装を通してPostScriptがSchemeで実装出来たらいいね。」

とか言ってたんですが。無茶苦茶メンド臭い。ハッキリ言って「無理じゃね?」とか思って来てました。多分僕はヘタレなのでしょう。ええ、間違いなく。それは否定しない。


まあ、でも多分一番問題なのは、Schemeには標準仕様としてmacroexpandが定義されていないに尽きると思います。手探りでマクロ展開形を想像しながらやる、ってのはシャレにならんのですよ。CLerが


「Schemeの仕様は貧弱だ」

と言う批判をするのは、この辺に付いては妥当だと思います。マクロ書くのに展開形が見れない、ってのはLisp系言語仕様設計としてはポイントがズレまくってます。困ったもんだ。


そんなわけで、「CLで伝統的マクロやる?万歳!」とか思ったんですね。率直な感想です。



※1: もっとも「あらゆる実装でmacroexpandが無い」と言う気はありません。むしろ実装依存ならあるって言った方が正しいです。

が。こう言う重要な機能が実装依存だ、ってのがぶっちゃけ納得出来ません。話によれば衛生的マクロのmacroexpandは実装が難しい、と言う話なんですが、だったら片手落ちにも程があるだろ、と。いや、納得せんぞ(笑)。その辺実装者側に投げて良い、って事もねえだろ、と。

Gaucheにはmacroexpandがあって、Guileはザーっとマニュアル見た限り存在せず、PLT Schemeはワケワカメです。

※2: 余談ですが、PLT のメーリングリストにも「PLTにはmacroexpandがないの?」ってトピックがあって、回答者側の一人が

    「DrSchemeのMacro Stepper使ってみろよ?macroexpandなんて使う気にならなくなるから。」

とかとても能天気な事を書いています(笑)。

実際、狙ったマクロ「だけ」を展開出来て、一見優れものに見えるんですが、

  • 実はPLTのメイン処理系であるMzSchemeでは動かない。Emacsで使う場合はわざわざMzSchemeを止めて、もう一つの処理系MrEdを呼び出さないと使えない。

  • その上、一々ライブラリとして(require macro-debugger/expand)しないといけない。

  • 構文が

    (syntax->datum
    (expand-only #'展開したい式
    (list #'展開したいマクロ名<複数列挙可>))))

    と打つのがメンド臭い程長い。


と三重苦です。マジな話やってられません


Common Lisp実装


んで、次回までANSI Common Lisp処理系を入れようと言う話です。 #9LISP で話に上がったANSI CL処理系は次の4つ。メモしておきます。



  • CMUCL: 米国カーネギー・メロン大学で開発されたANSI Common Lisp処理系(だからそのままCarnegie Mellon University Common Lisp)。ただし、現時点での開発継続は有志による筈。LOL一押しの処理系。CLのフリー実装としては1、2位を誇る処理速度を叩きだし、同じくフリー処理系のSBCL(後述)や商用のScieneer Common Lispのルーツである。

    ただし、基本的にWindowsはサポートしていない、UNIX用に特化した処理系と考えて良い。また、UNICODEもサポートしていない。

  • SBCL: CMUCLから友好的にフォークしたフリーのANSI Common Lisp処理系。CMUCLに独自に拡張を加えたもの、として考えて良い。CMUCLもSBCLもコンパイラはPythonと呼ばれるネイティヴコンパイラを使用している(LLのPythonとは無関係で、CMUCL/SBCLのユーザーは「あっちが後だ!」とたまに話を蒸し返す・笑)。

    最大の特徴はCMUCLと違い、UNICODEが扱える事。従って、次のようなコードを書いても良い。

    CL-USER> (defun のりピー (symb)
    (eq symb '酒井法子))
    のりピー
    CL-USER> (のりピー '酒井法子)
    T
    CL-USER> (のりピー '小倉優子)
    NIL
    CL-USER>

    日本語が扱えるフリーの最速処理系だと思われる。また、実験的だがWindows版も提供している。

    なお、SBCLはSteel Bank Common Lisp(鉄鋼-銀行 Common Lisp)の意。カーネギー・メロン大学の創設者の一人、アンドリュー・カーネギーが鉄鋼で財を成し、もう一人のアンドリュー・メロンは銀行業で財を成した事に由来する。

  • GNU CLISP: ドイツ製ANSI Common Lispのフリー処理系。1987年にATARI STで開発されたものが初版。readlineの使用に関してFSFのリチャード・ストールマンと揉め、後にGNUに寄贈される。

    特徴は、ソースのコンパイルを行うと、ネイティヴ・コードではなく、バイト・コードへとコンパイルする。故に実行速度よりオブジェクトコードの移植性を重視した実装となっている。スピードはCMUCL/SBCLに劣るが、竹内関数でのベンチマークを試してみると、コンパイルされたコードの実行速度はScheme実装Gaucheとさほど変わらなかった。故に、SchemeのGaucheが速い、と感じるなら、問題は特に無いと思われる。また、UNICODEにも対応している。

    WindowsだろうとUNIXだろうとどこでも動くので、世界で一番使われているフリー実装だと思われる。また、ポール・グレアムが作ったVia WebはCLISPを使っていた。Arcの最初の実装もCLISPで書いたらしい。

    開発はまあまあ活発で、ヴァージョンアップは大体定期的で、一年に一回くらい。

  • ABCL: Armed Bear Common Lisp。JavaによるANSI Common Lispのフリー実装。開発が活発なのかそうじゃないのか、いまいち良く分からない。

    ちなみに一時期、Ubuntu 7.04~7.10辺りのレポジトリに入っていたが、今は消えてしまった。人気がないのか?良く分からん実装である。


とまあ、主観交えて書けばこんな感じでしょうか。お勧めはPC-UNIXだったらSBCL、Windowsだったら素直にCLISPにしとけば基本問題無い、と思います。Macは良く知らん。


どの実装を選ぶか、ってのは頭が痛い問題なんで、ぶっちゃけ、オールインワンのLispbox入れときゃ充分なんじゃねえの?とか思います。その方が設定で面倒臭い思いしなくて済みますしね。



















































OS X (10.4/PPC) OS X (10.4/Intel) GNU/Linux x86 GNU/Linux x86-64 Windows
Allegro 8.1 8.1 8.1 8.1 8.1
SBCL 0.9.7 0.9.7
Clozure CL 1.0 1.0
CLISP 2.35 2.36 2.37


Common LispとSchemeの大まかな違い


SchemeはLisp-1、Common LispはLisp-2である


多分 @aharisu さんだったと思いますけど「Common LispとSchemeの違いって何?」と言う質問に対して、


「SchemeはLisp-1だけどCommon LispはLisp-2です」

って即答してました。多分一言で言うとそうだろうと思います。両者ともレキシカル・スコープを採用したLispですが、そうなると一言で言える違いはコレになるでしょう。もちろん、細かい話では色々な違いが出てきますが。



Lisp-2は名前空間を二つ持ち、片方を演算子に使い片方を変数に使う。一方、Lisp-1の名前空間は1つだ。


実際、Common Lispの方は名前、つまりシンボルの衝突を避ける為に神経質なまでに繊細な設計を行っています。逆に言うと、設計が繊細なお陰でユーザーはあまり面倒臭い事を気にかける必要がありません。大雑把にいられる。


例えば、このページで紹介されてる例が良い例だと思います。



;;; Common Lisp の場合
CL-USER> (setf sin 1.0)
;
; caught WARNING:
; undefined variable: SIN
;
; compilation unit finished
; Undefined variable:
; SIN
; caught 1 WARNING condition
1.0
CL-USER> (sin sin)
0.84147096
CL-USER>

;;; Schemeの場合

> (define sin 1.0)
> (sin sin)
procedure application: expected procedure, given: 1.0; arguments were: 1.0

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

SBCLだと警告が出ますが、それでも(sin sin)なんてヘンな式でもへーキで実行してしまいます。S式の第一要素は関数、第二要素は変数だ、と賢く解釈してくれるから、です。一方Schemeはそうしてくれません。何故なら、sinを1.0で束縛してしまった以上、sinはもはや手続きじゃなくなってしまったのです。



シンボル



上のsinみたいなのをシンボルと呼ぶわけですが。Lispの特徴としてはこのシンボルがデータ型であると言うのが前提なんですが、結局このシンボルの扱いがCommon LispとSchemeでは大きく違うわけです。


Schemeの仕様書(R5RS)には次のように定義されています。



シンボルとは,二つのシンボルが(eqv? の意味で) 同一なのは名前が同じようにつづられるときかつそのときに限られるという事実に,その有用性がかかっているオブジェクトである。これはまさに,プログラムで識別子を表現するために必要とされる性質であり,したがってScheme の大多数の実装はシンボルを内部的にその目的のために利用している。シンボルは他の多くの応用にも有用である。たとえば,列挙値をPascal で利用するのと同じ用途に,シンボルを利用してもよい。

色々ゴチャゴチャと書いていますが、要するに、


  1. 各シンボルはプログラム中で「唯一無二の存在」でなければならない。

  2. じゃないと識別子として役に立たない。

  3. つまり、シンボルは「名前を識別する為だけに」存在する。


と言ってるんですね。言わば当たり前の事を言ってるんですが。他の言語鑑みてもわざわざシンボル型なんてデータ型が存在しなくても良いくらいアッサリしています。


ところが、Common Lispの場合、シンボル自体に機能がある。詳しい事はCLHSを見てほしいんですが、端的な表現としてはシンボルに対してアクセサが3つも定義されている。C的に言うとまるで、シンボルは構造体で定義されているデータ型の如し、です。



例えば上のように(setf sin 1.0)とREPL上で評価した後、次のように、



CL-USER> (symbol-function 'sin)
#<FUNCTION SIN>
CL-USER>

とすればsinと言う組み込み関数が束縛されている事が分かります。


続いて次のようにすれば、


CL-USER> (symbol-value 'sin)
1.0
CL-USER>

sinに1.0が束縛されている事が分かります。つまり同一のsinと言うシンボルが二つの役割を担っていると言う事が明らさまに分かる。要するに、名前空間が二つ存在しているんです。


んじゃあ、最後のsymbol-plistってのは何なのか?本筋にあんま関係ないんで端折って書いておくと、シンボルはついでに属性リストと言う特別なリストを「内部構造として」持つことが可能なんです。symbol-plistはそいつをリテラルとして引っ張り出すアクセサです。


例えば大域変数として次のようなメンバー名のリストとする9lispを定義します。



CL-USER> (defparameter 9lisp '(valvallow mutronix 堀 田中克之 そーり shunsuk gibson cametan sakurako_s _Relm koki-h AKIRA))
9LISP
CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER>

シンボルを大域変数で定義した場合、REPLでシンボルを評価した結果とsymbol-valueでシンボルにアクセスした結果は同じです。



CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER> (symbol-value '9lisp)
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER>

ところが、シンボル9lispはここで定義したリストと別の属性リスト(キーとデータが交互に並んだリストの事)を内部に持つことが出来ます。



CL-USER> (setf (get '9lisp :parent-group) 'KPF
(get '9lisp :通称) '9lisp
(get '9lisp :explanation) '九州熊本を中心にLISPの勉強会を開催しています
(get '9lisp :開催) '隔週
(get '9lisp :オンライン参加) t)
T
CL-USER> (symbol-plist '9lisp)
(:オンライン参加 T :開催 隔週 :EXPLANATION 九州熊本を中心にLISPの勉強会を開催しています :通称 9LISP
:PARENT-GROUP KPF)
CL-USER>

そして、先ほど大域変数として値を与えた9lispとその内部構造である属性リストは、当然ながら名前を共有しながら全く別物なんです。



CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER> (symbol-plist '9lisp)
(:オンライン参加 T :開催 隔週 :EXPLANATION 九州熊本を中心にLISPの勉強会を開催しています :通称 9LISP
:PARENT-GROUP KPF)
CL-USER> (equal 9lisp (symbol-plist '9lisp))
NIL
CL-USER>

とまあ、Common Lispに於いてはシンボルは最低でも3つの役割を担っています。ただし、最後の属性リストに関して言うと、ポール・グレアムは、


なお、Common Lispでは属性リストはあまり使われない。ハッシュ表を使うことがはるかに多い。



と記述しています。と言うわけで、属性リストは使わなくても構わないらしいですが、同時にここではANSI Common LispにはSchemeでは存在しないハッシュ表も存在すると言う事が分かります。



いずれにせよ、同一のシンボルに対して最低でも3つの役割を持たせているのがANSI Common Lispで、これだけに限らず、たとえ同一のシンボルが存在しようと可能な限り衝突を避けようと設計されているのがANSI Common Lispの仕様です。ここでは立ち入りませんが、パッケージと言う仕組みがあって、何重にもシンボル衝突を回避しようとしてる。そしてそれが実はANSI Common Lispの設計上の肝なんです。


比較するとSchemeのシンボルはANSI Common Lispに比べるとお粗末、と言えばお粗末です。黒板の人の



(quote a) と入れたら A と返ってくるのが symbol なわけではなくて、 symbol っちゅうものは、そもそもプログラマから見れば first class object であって、 packageintern したり unintern したり、他の package から inherit したり、 shadow したり、 export したり import したりできるもの。それが symbol です。
Scheme には、この symbol がありません。


と言うScheme批判はこの辺なんですよね。少なくとも今まで見た通り、Schemeのシンボルだと出来る事は殆ど無いに等しい、と言う事ですから。



写像関数と関数適用の例


さて、名前空間が二つある、と言う事はシンボルが表すものが最低2つはある、と言う事です。その辺でSchemeと若干作法が変わってきたりします。代表的なトコで写像関数を見てみましょう。


Schemeでは写像手続きはmapですが、ANSI Common Lispでそれに一番近いものはmapcarでしょう。


そこで次のような問題を考えます。



要素がリストであるリストがある。各要素のcarを取ったリストを返せ。

Schemeだったらこう書くでしょう。



> (map car '((Google YouTube) (ニワンゴ ニコニコ動画)))
(Google ニワンゴ)
>

ところが、これがCommon Lispじゃあ上手く動かない。



CL-USER> (mapcar car '((Google YouTube) (ニワンゴ ニコニコ動画)))
The variable CAR is unbound.
[Condition of type UNBOUND-VARIABLE]
; Evaluation aborted.
CL-USER>

何と「変数carは未束縛です」と文句を言ってくる。これは何故かというと、ANSI Common LispではS式の第一要素に置かれたシンボルに付いては素直に「関数」だと解釈してくれるんですが、第二要素以降に付いてはデフォルトでは必ず「変数」だ、と解釈してくるんです。一方、Lisp-1であるSchemeでは、S式のどこに手続きとして定義されたシンボルを置いても、手続きは手続き、なわけです。この辺がCommon LispとSchemeでは食い違ってくるんです。


そこで、ANSI Common Lispでは、S式の第二要素以降に置いたシンボルが変数なのか関数なのか、処理系に知らせてやらないとならない。上の問題はANSI Common Lispではこう書きます。



CL-USER> (mapcar #'car '((Google YouTube) (ニワンゴ ニコニコ動画)))
(GOOGLE ニワンゴ)
CL-USER>

carの前に置いた記号はそのままシャープクオートと呼びます。これ、実際は省略記法で、@valvallowさんに「何の省略記号だっけ?」と訊かれてぶっちゃけ忘れてたんですけど(笑)、functionと言う特殊オペレータ(Schemeで言う特殊形式)の省略記法ですね。故に上の式は次と等価です。



CL-USER> (mapcar (function car) '((Google YouTube) (ニワンゴ ニコニコ動画)))
(GOOGLE ニワンゴ)
CL-USER>

実際(function 何とか)とか書いてられねえけどな(笑)。メンドっちくて(笑)。だから#'だけ覚えていてその意味忘れてた僕は責められないと思う(笑)。多分(笑)。


いずれにせよ、ANSI Common Lispでは、写像関数を含む高階関数が関数引数を取る場合、渡す関数引数には#'を付ける、と覚えておきましょう。



ANSI Common Lispでの関数定義



実際Schemeは変わった言語で、事実上、例えばPascal的な意味での「手続き定義」の様式を持ちません。特殊形式defineが行ってるのはシンボルとデータ(つや~な言い方をするとオブジェクト)を結びつけてる(束縛)してるだけ、です。当然λ式で形作られるクロージャもデータなんで、全ての「値」を一元的に扱える、と言うのがSchemeの特徴なわけです。



> (define a 1) ;シンボル a と数値 1 を結びつけた例
> a
1
> (define fact ;シンボル fact と クロージャを結び付けた例
(lambda (n) ;これが Scheme に於ける手続き定義の基本となる
(letrec ((iter
(lambda (m acc)
(if (zero? m)
acc
(iter (- m 1) (* m acc))))))
(iter n 1))))
> fact
#<procedure:fact> ;Scheme に於ける手続きリテラルの例
>

一方、Lisp-2であるANSI Common Lispの場合、シンボルには複数の役割がある、と言う事を既に見ました。従って、表層的にせよ、変数定義と関数定義は分かれてなければならないと言う要請が出てきます(※)。ANSI Common Lispで用いられる関数定義の為のマクロがdefunです。



CL-USER> (defun fact (n)
(labels ((iter (m acc)
(if (zerop m)
acc
(iter (1- m) (* m acc)))))
(iter n 1)))
FACT
CL-USER> (symbol-function 'fact)
#<FUNCTION FACT>
CL-USER>

一見、SchemeのMIT記法による手続き定義の構文糖衣に似てますね。並べて比べてみれば分かると思います。











ANSI Common Lisp の関数定義の例 Scheme の手続き定義の例


(defun fact (n)
(labels ((iter (m acc)
(if (zerop m)
acc
(iter (1- m) (* m acc)))))
(iter n 1)))



(define (fact n)
(letrec ((iter (lambda (m acc)
(if (zero? m)
acc
(fact (- m 1) (* m acc))))))
(iter n 1)))


パラメータリスト(引数リスト)に関数名/手続き名を表すシンボルが第一要素として含まれるか否か、って辺りだけが違います。ANSI Common Lispの場合は含まない、SchemeのMIT型構文糖衣だと含む、と言うのが違いです。


実際問題、SchemeとCommon Lisp行き来してると、この辺良く間違うんですよ(笑)。手癖と言うか(笑)。関数定義/手続き定義をやってエラーが出て、一瞬、


「あれ?どこ間違えたんだろ?」

ってクダラないトコで悩む事が良くあります(笑)。僕だけか(笑)?



※: 本当は出てきません。あくまでユーザー的に表層では、と言う事です。


Common Lispの大域変数定義


Lisp-1とLisp-2に絞って大まかなSchemeとANSI Common Lispの違いを見ると、残るは大域変数だけ、です。が、これがややこしいんで端折ります。


Schemeの場合、結局何でもdefineなんですが、ANSI Common Lispの場合は大域変数定義には次の3つがあるんです。



んで、これらの使い分け、ってのが細かくてぶっちゃけピンと来ていません(笑)。実際僕だけでもないみたいで、defconstantはともかくとして、defvar派とかdefparameter派、みたいなのがいたりする模様です(※)。


ザーッと手元の資料見てみても、割に実践Common Lispが一番詳細に解説してたりしますね。が、長いんで転載出来ません(笑)。英語で良ければ原書の第6章辺りを参考にしてください。



※:本によって解説が偏ってる、ってのは事実。例えばポール・グレアムは「自分の好きな機能」に関してはページを多く割く傾向があって、defvarはあんまり使わないんだけどdefparameterは良く使う、ってのが見て取れる。他の本も大同小異だったりする。

CLerの間でこの手の「バカに豊富な機能のどれを使うか?」と言うので意見が分かれる事が良くあるようで、例えば簡単なトコでは等価述語であるeqeqlに対しても「何でもeq派」と「何でもeql派」に分かれているらしい。前者はスピード重視派、後者はデフォルト推奨派、ならしい。

ちなみに、Emacs Lispにはdefparameterdefconstantは存在しない模様でビックリした。恐らくこの中ではdefvarが歴史的には一番由緒正しいのだろう。


Common LispとSchemeのちょっとだけ細かい違い



ではもうちょっとだけANSI Common LispとSchemeの違いを見てみましょう。これらはtrivialな話ではなくって、実際SchemeからはじめてCommon Lispへと進んでプログラムを書く場合、場合によっては障害となるんじゃないか、と言う個人的な観点に絞ります。



ANSI Common Lispはどんな形であれ値を返す


Schemeの仕様書を見ていくと、意外と「返り値は未定義である」と言う表現を多く見かけます。それが結構多くて困った事が多いんですが(実際多い)、一方、ANSI Common Lispは、一見無意味に見えるものでも必ず値を返します。代表的なところは出力関数でしょうか。












Common Lisp の format 関数の例

Scheme の display 手続きの例


CL-USER> (format t "~A~%" "Hello, World!")
Hello, World! ;表示
NIL ;format の返り値自体は nil
CL-USER>



> (display "Hello, World!\n")
Hello, World! ;Hello, World! は表示だが、display の返り値は未定義
>


一見無意味に見えるんですが、Common Lispの出力関数formatは返り値があります。一方、Schemeのdisplayの返り値は未定義です。


「どんな場合でも何らかの値を返す」と言う事の信頼性は重要です。ANSI Common Lispに関して言うと、信頼して構わない、と言う事です。



ANSI Common Lispの真偽値


ANSI Common Lispでの真値はt、偽値はnilで表され、Schemeではそれぞれ#t#fで表されます。


これだけ見ると大した違いが無いように見えるんですが、どころがどっこい。ANSI Common Lispでは次のようなルールが存在します。



  • 空リストはnilの事である。

  • nilはユニークなシンボルである。

  • 論理上、nilでないものは、全てtである。


従ってANSI Common LispとSchemeでは次のような食い違いが起こります。











ANSI Common Lispの場合 Schemeの場合


CL-USER> (null '())
T
CL-USER> (not '())
T
CL-USER>


> (null? '())
#t
> (not '())
#f
>

実はANSI Common Lispではnullnotは同機能異名の関数なんですが、Schemeではnull?は空リスト判定用述語、notはあくまで真偽値判定用述語です。そしてSchemeに於いては空リストは#fではない。要するにANSI Common LispとSchemeではブーリアンの体系が違うんですよね。Schemeはいわゆる普通のプログラミング言語と同様に、ブーリアン用の体系があるわけですけど、ANSI Common Lispはちょっとしたハックに見える。


従って、ANSI Common Lispでは次のような一見わけの分からない事が可能です。



CL-USER> (cons 1 (> 2 3))
(1)
CL-USER>

(> 2 3)は偽なんでnilを返すわけなんですけど、それはすなわち空リストです。そこに1をconsして、リストにしちゃう……。「こんなんで大丈夫なのか?」とか思うでしょうが、実はこのnilの性質は色んな局面で結構役に立ちます(※)。プログラムが短くなったりするんですよ。



※: ちなみに、僕が最初に触ったのはANSI Common Lispの方で、「随分大雑把な設計だな」とか感じました。が、慣れると病みつきになって、最初Schemeを触った時は#t#fの存在がどうにも馴染めませんでした。っつーか、ハッキリ言うと嫌いだった。


例えば、The Little Schemerに次のようなお題があるらしいんですけど。




;;; The Little Schemer の member? の回答例
> (define (member? a lat)
(and (pair? lat) ;いちいちlatがどうなのか検査しなければならない。
(or (eq? a (car lat))
(member? a (cdr lat)))))
> (member? 'a '(f u c k o f f!))
#f
>


ANSI Common Lispだったら次のように書けます。



CL-USER> (defun member? (a lat)
(and lat ;これだけで充分
(or (eq a (car lat))
(member? a (cdr lat)))))
MEMBER?
CL-USER> (member? 'a '(s o n o f a b i t c h !))
T
CL-USER> (member? 'a '(f u c k o f f !))
NIL
CL-USER>


リスト再帰は構造上、ベースケースとして空リストが停止条件になる場合が多いんですが、空リスト=nilと定義しているCommon Lispではこの性質が大活躍します。上の例だと、andnilを見つけると即刻評価を中止してnilを返すのが肝なんですけど、latはリストである事が前提で、かつ、そこに対して特殊な判定は必要ないわけです。latが空リストになった途端、andはそれを偽値と解釈して再帰は滞り無く終わる。



上の例は単純ですが、色んな場面で効いてくる、Common Lispの便利な側面です。




偽と空リストに対して同じものを使うと混乱を引き起こすことがときどきあるが、長年のLispプログラミングの中で私はそれが掛け値なしの勝利であることを確信している。なぜなら、空リストは集合論での偽であり、多くのLispプログラムは集合で考えるからである。




ANSI Common Lispのcarcdr


さて、偽値=空リストであるANSI Common Lispのnilなんですが。Schemeと違って次の大変美しい性質があります。それは空リストにcarcdrを適用したらどうなるか、と言う話です。


Schemeの場合、空リストにcarcdrを適用するとエラーを返してきます。



> (car '())
car: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

> (cdr '())
cdr: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

一方、ANSI Common Lispではそう言う事がありません。



CL-USER> (car '())
NIL
CL-USER> (cdr '())
NIL
CL-USER>


nilcar/cdrを適用するとnilが返る。これは大変ありがたい仕様です。



これは実はInterlisp由来の仕様だそうなんですが、いずれにせよ大変便利で、それがCommon Lispの仕様として取り入れられたそうです。



ANSI Common LispとSchemeのcar/cdrの違いは、そのままそれらを利用した組み込み関数/手続きの挙動にも影響しています。代表的なのは次の機能でしょうか。
















Scheme ANSI Common Lisp
list-ref nth
list-tail nthcdr


両者とも引数の順序が違うんですが、大体同じ機能です。ただし、nilcar/cdrの扱いの違いで、挙動に差が生じるのです。


例えば、LOLで紹介されている(正確に言うとOn Lispで紹介されている)groupと言う関数があります。コードは以下の通りです。



(defun group (source n)
(if (zerop n) (error "zero length"))
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons (subseq source 0 n) acc))
(nreverse (cons source acc))))))
(if source (rec source nil) nil)))

Schemeでそのまま何も考えないで直訳すると次のような感じでしょうか。




;; Gauche なら (use srfi-1)
;; Guile なら (use-modules (srfi srfi-1))
(require srfi/1) ;PLT Scheme 依存

(define (group source n)
(and (zero? n) (error "zero length"))
(letrec ((rec (lambda (source acc)
(let ((rest (list-tail source n))) ;list-tail の引数の順番に注意!
(if (pair? rest)
(rec rest (cons (take source n) acc)) ;take が srfi-1 の手続き
(reverse (cons source acc)))))))
(and (pair? source) (rec source '()))))


ところがどっこい、これは動きません。




> (group '(a b c d e f g) 2)
list-tail: index 2 too large for list: (g)

=== context ===
stdin::7328: group
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>


list-refが「sourceリストの長さが足りない!」と文句を言ってくる。つまり、こう言う事です。












ANSI Common Lispのnth-cdrの挙動 Schemeのlist-tailの挙動


CL-USER> (nthcdr 2 '(g))
NIL
CL-USER>



> (list-ref '(g) 2)
list-ref: index 2 too large for list: (g)

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>



groupは与えられたsourceリストに対して、再帰的に要素をn個ずつグルーピングしていくわけですが、結局ベースケースで問題が生じてくるわけです。ANSI Common Lispでは考えなくて済むんですが、Schemeの場合、「現時点でのリストがどう言う状況なのか?」一々チェックを入れないとなりません。この場合は常にsourceの長さがnより大きいかどうか見なければならない。


従って、Schemeだとここまで面倒みないとなりません。




> (define (group source n)
(and (zero? n) (error "zero length"))
(letrec ((rec (lambda (source acc)
(let ((rest (if (< (length source) n) ;リスト source の長さを n と比較する
'() ;空リストを返す
(list-tail source n))))
(if (pair? rest)
(rec rest (cons (take source n) acc))
(reverse (cons source acc)))))))
(and (pair? source) (rec source '()))))
> (list-ref '(g) 2)
> (group '(a b c d e f g) 2)
((a b) (c d) (e f) (g))
>


CLerだったら


「やってらんねえよ!!!」

とか言うんでしょうね(笑)。人間一旦ラクを覚えたら引き返せませんので(笑)。



いずれにせよ、ANSI Common Lispではnilに対するcar/cdrがエラーにならないお陰で、書くべきプログラムが短く済む事が多いのです。



ANSI Common Lispにはラムダ・リスト・キーワードがいっぱい



Schemeの手続きに於いては必須パラメータとレスト・パラメータくらいしかありません。まあ、実装によるんですが、仕様(R5RS)ではそうなっていますね。


一方、ANSI Common Lispでは必須パラメータ「その他」がたくさんあります。必須パラメータ「以外」をラムダ・リスト・キーワード等と呼ぶようです。


#9LISP の方で@aharisuさんが紹介していたんですが、ここでもザックリまとめておきたいと思います。





レスト・パラメータはSchemeのヤツと同じなんで、ここでは端折ります。キーワード・パラメータに付いては後回しにします。まずはオプショナル・パラメータを見てみます。



例えば、Schemeを扱っていて、末尾再帰で手続きを書くのに便利な構文にnamed-letなんてのがあります。ワンパターンで階乗手続きを定義しますが、次のように使いますね。



> (define (fact n)
(let loop ((n n) (acc 1))
(if (zero? n)
acc
(loop (- n 1) (* n acc)))))
> (fact 10)
3628800
>

実際これは便利で、LOLも本格的なマクロ実装の最初の例として、このnamed-letをまずは実装しよう、と言う流れになっています。


どうしてこんなに楽に末尾再帰が書ける構文をCommon Lispは持ってないんだろう?平たく言うとその必要が無いからって事でしょうね(※)。ANSI Common Lispにはオプショナル・パラメータがある。




オプショナル・パラメータとは文字通りオプショナルなパラメータで、関数呼び出しのときに対応する引数があってもなくてもよい。

~(中略)~

オプショナル・パラメータは、もし対応する引数があればその引数を初期値とし、そうでなければ、ラムダ・リストに指定された値(デフォルト値という)を初期値とする。個々のオプショナル・パラメータは、一般に三つの要素からなるリストで指定する。


    (<<変数>> <<式>> <<判定変数>>)

~(中略)~

判定変数が必要なければ、オプショナル・パラメータ指定の<<判定変数>>のところを省略して

    (<<変数>> <<式>>)

だけでもよい。また、デフォルト値を特に指定する必要がなければ

    (<<変数>>)

だけでもよい。これらの場合はデフォルト値としてnilが使われる。




つまり、オプショナル・パラメータに適当な初期値を与えれば、named-let無しでも簡単に末尾再帰が書けて、結果Scheme版より短くコードを記述する事が可能となるわけです。



CL-USER> (defun fact (n &optional (acc 1)) ;オプショナル・パラメータとして acc を初期値1として設定
(if (zerop n)
acc ;返り値は acc
(fact (1- n) (* n acc)))) ;オプショナル・パラメータ acc を操作する
FACT
CL-USER> (fact 10) ;計算はオプショナル・パラメータ無しで
3628800
CL-USER>


もう一つ例を挙げましょう。これはオプショナル・パラメータがどうの、と言うよりLisp-2であるための説明なんですが、ここで見てみます。


The Little Schemerrember-fと言う問題があるそうで、これは高階手続きの問題のようですが、Schemeのnamed-letを用いると次のように定義出来ます。



> (define (rember-f test? a l)
(let loop ((l l) (acc '()))
(if (null? l)
(reverse acc)
(loop (cdr l) (let ((it (car l)))
(if (test? a it)
acc
(cons it acc)))))))
> (rember-f (lambda (x y)
(= x y)) 5 '(1 2 3 4 5))
(1 2 3 4)
> (rember-f (lambda (x y)
(eq? x y)) 'jelly '(jelly beans are good))
(beans are good)
> (rember-f (lambda (x y)
(equal? x y)) '(pop corn) '(lemonade (pop corn) and (cake)))
(lemonade and (cake))
>


これもANSI Common Lispでは、上で見た通り、オプショナル・パラメータを用いた末尾再帰で基本的には実装可能なんですが、実際やってみると、何故かエラーを返してきます。




CL-USER>> (defun rember-f (test? a l &optional (acc nil))
(if (null l)
(reverse acc)
(rember-f test? a (cdr l)
(let ((it (car l)))
(if (test? a it)
acc
(cons it acc))))))

; (TEST? A IT)
;
; caught STYLE-WARNING:
; undefined function: TEST?
;
; compilation unit finished
; Undefined function:
; TEST?
; caught 1 STYLE-WARNING condition
REMBER-F
CL-USER>


上はSBCLでの警告表示ですが、(test? a it)test?なんて関数はないぞ、と文句を言ってくる。Schemeだと何ともないんですが、Common Lispじゃダメだ、って事ですね


何が悪いのか、と言うと、仮引数はデフォルトでは変数が来る、と解釈されているわけです。ところがtest?と言う仮引数は関数のポジションであるS式の第一要素に鎮座してる。Common Lispは「それじゃマズいだろ」って言ってるわけです。


これを解決するのが、Schemerの中で悪名高いCommon Lispのfuncallです。つまり、こう書けば良い。




CL-USER> (defun rember-f (test? a l &optional (acc nil))
(if (null l)
(reverse acc)
(rember-f test? a (cdr l)
(let ((it (car l)))
(if (funcall test? a it) ;ここで使う
acc
(cons it acc))))))
REMBER-F
CL-USER> (rember-f #'(lambda (x y)
(= x y)) 5 '(1 2 3 4 5))
(1 2 3 4)
CL-USER> (rember-f #'(lambda (x y)
(eq x y)) 'jelly '(jelly beans are good))
(BEANS ARE GOOD)
CL-USER> (rember-f #'(lambda (x y)
(equal x y)) '(pop corn) '(lemonade (pop corn) and (cake)))
(LEMONADE AND (CAKE))
CL-USER>


最後に補助変数&auxです。これはあんまり使われてるの見たことないんで、Common Lisp入門から解説を引っ張ってきます。




補助変数とは関数内で局所的に使用する変数のことで、関数の引数とは関係ない。局所変数は、letlet*などのスペシャル・フォームを使っても宣言できるが、ラムダ・リスト・キーワード&auxを使うことによって、関数定義がコンパクトになる。例えば

    (lambda (a b) (let* ((c 1) (d 2)) ...))

と書くかわりに&auxを使って、

    (lambda (a b &aux (c 1) (d 2)) ...)

と書くことができる。各補助変数指定は、変数名とその初期値を与える式からなるリストである。

    (<<変数>> <<式>>)

<<式>>がnilの場合はこれを

    (<<変数>>)

あるいは単に

    <<変数>>

と略してもよい。


だそうです。


確かに便利そうですが、あんま使われてるのを見ないのは、ひょっとしたらCLerはletの方が好きなのかもしれませんね。




※: もっとも、LOLの流れから言うと、「もっとも簡単なマクロで作る制御構文」の例としてnamed-letを敢えて取り上げたと思われる。

もう一つの狙いが「マクロに最適化したコードを吐かせる」例として、魅力的な題材だったのだろう。実際、ANSI Common Lispの仕様はSchemeと違って末尾再帰の最適化を要求していない。

ただし、「末尾再帰の最適化をしない」Common Lispの実装を思いつく方が難しい、と言うのも事実である。ちなみに、GNU CLISPの場合、インタプリタ上で入力しただけの末尾再帰関数に関しては最適化しないが、バイトコードへコンパイルすると最適化を行う模様である。


ANSI Common Lispの組み込み関数は汎用/統合指向



Schemeにはmemqmemvmemberと言う組み込み手続きがあります。それぞれ二つ引数を取り、第二引数はリストで、第一要素と等価なcarを持つ第二引数の部分リストを返します。ただし、この「等価」の定義が違って、memqeq?memveqv?memberequal?を比較に用います。




> (memq 'a '(a b c))
(a b c)
> (memq 'b '(a b c))
(b c)
> (memq 'a '(b c d))
#f
> (memq (list 'a) '(b (a) c))
#f
> (member (list 'a)
'(b (a) c))
((a) c)
> (memq 101 '(100 101 102)) ;ここの動作は未定義
(101 102) ;実装によって結果が異なる
> (memv 101 '(100 101 102))
(101 102)
>


一方、ANSI Common Lispにはmemberしかありません。ただし、ANSI Common Lispのmemberは高階関数として設計されていて、比較演算子をキーワード・パラメータとして受け取る事が出来ます。




CL-USER> (member 'a '(a b c) :test #'eq) ; :test がキーワードパラメータで、ここで比較関数を指定
(A B C)
CL-USER> (member 'b '(a b c) :test #'eq)
(B C)
CL-USER> (member 'a '(b c d) :test #'eq)
NIL
CL-USER> (member (list 'a) '(b (a) c) :test #'eq)
NIL
CL-USER> (member (list 'a)
'(b (a) c) :test #'equal)
((A) C)
CL-USER> (member 101 '(100 101 102) :test #'eq) ;ここは実装依存の結果が返る
(101 102)
CL-USER> (member 101 '(100 101 102)) ;デフォルトは eql による比較
(101 102)
CL-USER>


それどころか、CLHSに記載されている通り、比較対象の要素まで指定する事が出来ます。




;; ドット対のcdrを見て等価じゃないところから返せ、と言う指定
CL-USER> (member 2 '((1 . 2) (3 . 4)) :test-not #'= :key #'cdr)
((3 . 4))
CL-USER>


このように、ANSI Common Lispの一部の関数は、Schemeではバラバラになっている手続きを統合したような単一関数として設計されているパターンが多いです(※)。もはや「組み込み関数」と言うより立派な一つのプログラムである、と言って良いくらいでしょう。非常に大掛かりなのがANSI Common Lispの関数の特徴です。



もう一つこの手の代表的なものに、連想リストへのアクセサ、assocが挙げられます。Schemeではassqassvassocが用意されています。




> (define e '((a 1) (b 2) (c 3)))
> (assq 'a e)
(a 1)
> (assq 'b e)
(b 2)
> (assq 'd e)
#f
> (assq (list 'a) '(((a)) ((b)) ((c))))
#f
> (assoc (list 'a) '(((a)) ((b)) ((c))))
((a))
> (assq 5 '((2 3) (5 7) (11 13))) ;これは返り値は未定義
(5 7) ;実装依存の結果が返る
> (assv 5 '((2 3) (5 7) (11 13)))
(5 7)
>


当然ANSI Common Lispではassocは高階関数として設計されていて、比較関数はキーワード・パラメータで変更するようになっています。




CL-USER> (defparameter e '((a 1) (b 2) (c 3)))
E
CL-USER> (assoc 'a e :test #'eq)
(A 1)
CL-USER> (assoc 'b e :test #'eq)
(B 2)
CL-USER> (assoc 'd e :test #'eq)
NIL
CL-USER> (assoc (list 'a) '(((a)) ((b)) ((c))) :test #'eq)
NIL
CL-USER> (assoc (list 'a) '(((a)) ((b)) ((c))) :test #'equal)
((A))
CL-USER> (assoc 5 '((2 3) (5 7) (11 13)) :test #'eq) ;返り値は実装依存
(5 7)
CL-USER> (assoc 5 '((2 3) (5 7) (11 13))) ;デフォルトは eql による比較
(5 7)
CL-USER>



※: 最初のSchemeは1975年に登場、ANSI以前のCommon Lispは1984年登場なんで、約10年の開きがあるわけです。仕様自体はCommon Lispの方が新しい。

調べてみた限り、Schemeのmemq/memv/memberassq/assv/assocは共にMITのMacLisp由来で、Schemeでの名称もこれを受けています。一方、約10年後に登場したCommon Lispではキーワード・パラメータの導入と共に、積極的に統合化へ進んだ、と見た方が良いでしょう。

なお、MacLisp直系の子孫であるEmacs Lispでは、やはりSchemeのようにmemq/memv/memberassq/assv/assocと言う形になっている模様です。


Schemeにはいくつもの「データ変換用」の手続きが用意されていて、それらは大体シンボルに->と言う記号が入っています。中でも良く使うのがリスト、ベクタ、文字列等の通称シーケンスと呼ばれるデータ型同士の変換手続きです。



  • list->string

  • list->vector

  • string->list

  • vector->list




> (list->string '(#\爆 #\発 #\し #\ろ #\!))
"爆発しろ!"
> (list->vector '(0 1 2 3 4 5 6 7 8 9))
#(0 1 2 3 4 5 6 7 8 9)
> (string->list "爆発した!")
(#\爆 #\発 #\し #\た #\!)
> (vector->list #(0 1 2 3 4 5 6 7 8 9))
(0 1 2 3 4 5 6 7 8 9)
>


これらは当然便利なんです。が、CLHSでそれらに対応する関数を探そうとしてもなかなか見つからない。まあ、見つからないのは当然で、ANSI Common Lispではこれらを統一的に扱う高階関数coerceに置き換わっています(しかも何て読むんだか分からない・笑)。




CL-USER> (coerce '(#\爆 #\発 #\し #\ろ #\!) 'string)
"爆発しろ!"
CL-USER> (coerce '(0 1 2 3 4 5 6 7 8 9) 'vector)
#(0 1 2 3 4 5 6 7 8 9)
CL-USER> (coerce "爆発した!" 'list) ;ユニコード指定文字のリストに変換される
(#\U7206 #\U767A #\HIRAGANA_LETTER_SI #\HIRAGANA_LETTER_TA #\!)
CL-USER> (coerce #(0 1 2 3 4 5 6 7 8 9) 'list)
(0 1 2 3 4 5 6 7 8 9)
CL-USER>


ちなみに、その他の変換手続きへの対応表は以下の通りとなっています。








































Scheme ANSI Common Lisp
char->integer char-code
exact->inexact float
inexact->exact rational
integer->char code-char
number->string write-to-string
string->number parse-integer
string->symbol intern
symbol->string symbol-name/string


ANSI Common Lispにはfoldがない



けどreduceはあります。@valvallowさん大喜び(謎)。




;;; http://valvallow.blogspot.com/2010/04/lol-flatten.html
;;; を参照
;;; 形式的には全く同じコードだと分かる
;;; CL の reduce は SRFI-1 のfold/fold-right/reduce/reduce-right
;;; をキーワード・パラメータを用いて統合化したようなもの

CL-USER> (defun flatten (tree)
(reduce #'(lambda (e acc)
(cond
((null e) acc)
((consp e)
(append (flatten e) acc))
(t (cons e acc))))
tree :from-end t :initial-value nil)) ;初期値に nil を指定して tree を逆順に辿る
FLATTEN
CL-USER> (flatten '(1 2 3 (4 5 6 (7) ((8) 9)) ((() 10))))
(1 2 3 4 5 6 7 8 9 10)
CL-USER>


ANSI Common Lispのsetfは超強力



Schemeのset!にあたるANSI Common Lispの推奨マクロがsetfです。setf汎用構造修正子としてデザインされています。


両者の使い方は基本的には同じです。











Scheme ANSI Common Lisp


> (define a 1) ;define の返り値は未定義
> (set! a '(x y z)) ;set! の返り値も未定義
> a
(x y z)
>


CL-USER> (defparameter a 1) ;defparameter の返り値は束縛されたシンボル
A
CL-USER> (setf a '(x y z)) ;setf の返り値は束縛されたデータ
(X Y Z)
CL-USER> a
(X Y Z)
CL-USER>


ただし、まずsetfの特徴として、複数の変数を逐次、破壊的に変更する事が可能です(※1)。これがSchemeのset!には出来ない。












Scheme ANSI Common Lisp


> (define a 1)
> (define b 2)
> (define c 3)
;;; 複数の変数を一気に変更しようとすると、当然エラーが出る
> (set! a 'x b 'y c 'z)
stdin::10037: set!: bad syntax (has 6 parts after keyword) in: (set! a (quote x) b (quote y) c (quote z))

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

> (set! a 'x)
> (set! b 'y)
> (set! c 'z)
> a
x
> b
y
> c
z
>


CL-USER> (defparameter a 1)
A
CL-USER> (defparameter b 2)
B
CL-USER> (defparameter c 3)
C
;;; 複数の変数を一気に変更する事が出来る
CL-USER> (setf a 'x
b 'y
c 'z)
Z ;返り値は最後に束縛されたデータ
CL-USER> a
X
CL-USER> b
Y
CL-USER> c
Z
CL-USER>


また、setfの第一引数は式でも良く、第一引数で参照された場所の値を破壊的に変更する事が可能で、これもSchemeのset!には出来ない事です(※2)。




CL-USER> (defparameter x '(a b c))
X
CL-USER> (setf (car x) 'n)
N
CL-USER> x
(N B C)
CL-USER>



※1: しかしながら「出来る」と言うのと「人気がある」と言うのは別っぽい。Common Lispにはもうちょっとプリミティヴな特殊形式であるsetqがあり、これも逐次で変数の値を破壊的に書き換える機能がある。同じ特殊形式がEmacs Lispにも存在するが、色んなelispファイルを眺めても、この「複数の値を一気に書き換える」書き方はあまり成されてないようである。それよりもsetqを羅列する方が好まれてる模様だ。

※2: SRFI 17でANSI Common Lispのsetfと同等の機能を持つ一般化set!が提案されてはいる。が、動かない処理系が出てきてるので要注意。

一般化set!をマクロで実装するにはset-car!set-cdr!と言う手続きが必要なのだが、これらはR6RS辺りからさほど重要な手続きではなくなってきた模様である。

    set-car!, set-cdr!: ペアの要素を変更可能にすると 特に実装者にとって色々面倒なことが起きるので、R6RS案の段階ではペアは 全て変更不可にしようぜ、みたいな極端な話が出てきたこともありました。 結局、これらの手続きを別ライブラリにすることでなんとなく妥協。 ペアを変更不可にすると、循環リストが出来ないんで色々見通しが良くなるんですね。 個人的には破壊的変更が嫌ならHaskell使えばって思いますが。



これを受けて、PLT SchemeがVersion: 4.0.0をもってset-car!/set-cdr!を廃止(Getting rid of set-car! and set-cdr!)。他にもScheme48のような海外でメジャーな処理系もset-car!/set-cdr!を備えていない。

Schemeの仕様に準じてないのにSchemeを名乗るのはおかしいだろ、って話もあるが、PLTは既に「Scheme方言である」と明言し、また、そもそもANSIと違って仕様書の縛りがそれほどキツくない。実装者側にとってのSchemeの魅力とは、言語仕様が比較的小さく、コアをラクに実装出来(とは言ってもそれはそれで難しい)、かつ自分のオリジナルのアイディアを組み込みやすいことである。従って処理系間の互換性が落ちやすい、と言うのがどの道前提になる(これはANSI Scheme仕様書であるR4RS準拠実装を今全くと言っていいほど見かけない、って事からも分かる)。

現時点ではR6RS準拠を謳っている実装はPLTくらいしかないが、今後Schemeはどうなっていくか分からない。いずれにせよ、当分の間set-car!/set-cdr!はアテにせん方が良いような気がする。


ANSI Common Lispはcase-insensitive



R5RSには明言されてないと思うんですけど、現存する実装を見る限り、Schemeはcase-sensitive(シンボルの大文字/小文字を区別する)言語です。R6RSでは、ハッキリとcase-sensitiveな言語となった模様です。




Lispの長年の伝統をついに破って、識別子がcase-sensitiveになりました。 もっともこれは現状追認とも言えます (参考:x:Concept:CaseSensitivity)。

処理系はオプショナルなcase-insensitive modeを用意しても 良いことになっています (6app:B)。


一方、ANSI Common Lispの方は、既に気づいたと思いますが、Lispの伝統に則り、case-insensitive(シンボルの大文字/小文字を区別しない)言語です。従って、両者のシンボルの等価判定には差が出てきます。












Schemeの場合 ANSI Common Lispの場合


> (eq? 'hoge 'HOGE)
#f
>


CL-USER> (eq 'hoge 'HOGE)
T
CL-USER>


ANSI Common LispはC登場以前の言語群の伝統を継承しているんですね。シンボル表記に関して言うと、ANSI Common LispはFortran/BASIC/Pascalの仲間です(っつーかUNIX文化じゃない)。



ANSI Common Lispには継続も遅延評価も無い



そんなつや~なものはANSI Common Lispにはありません(笑)。元々、Scheme自体が研究用途だった、と言う事もあって、実験的/先鋭的機能(※)を入れてるんですが、ANSI Common Lispはもっと堅実な仕様になっています。「より普通の」プログラミング言語なんですね。




※: とは言っても、R6RSの本体から遅延評価は消えた模様です。


    delay, force: R6RS本体では特に遅延評価のためのプリミティブは 提供されません。というのは、遅延評価メカニズム自体はSchemeの手続きの上に簡単に 構築できるのと、R5RSのdelayとforceはunderspecifiedで、実用に供するためには 結局再実装が必要であった (srfi-40およびsrfi-45をめぐる議論参照。x:SRFI-40, x:SRFI-45)、という経験によります。




ANSI Common Lispでは、大域脱出をする際には通常、特殊オペレータreturn-fromか、あるいはreturnマクロを用います。



例えば、次のような問題を考えます。




引数のリストの要素が全てpred?を満たすか判定するlist-of-pred?手続き/関数を定義してみる











Scheme: call/cc での解 ANSI Common Lisp: return-from での解

(define (list-of-pred? lst pred?)
(call/cc
(lambda (return)
(and (map (lambda (x)
(let ((it (pred? x)))
(if it it (return it)))) lst)
#t))))

(defun list-of-pred? (lst pred?)
(and (mapcar #'(lambda (x)
(let ((it (funcall pred? x)))
;; return-from は第一引数に関数名を取り、
;; そこから第二引数の値を持って大域脱出する
;; 第二引数が省略された場合のデフォルト値は nil
(if it it (return-from list-of-pred?)))) lst)
t))


なお、On LispではCommon Lispでの遅延評価の実装例継続の実装例が紹介されています。



ANSI Common Lispは(過剰に)親切設計



ANSI Common Lispは非常に巨大な仕様で有名なんですが、何と仕様にデバッグ関連のツールまで含まれているんです。デバッグ関連ツールまで(ある程度にせよ)定義されている言語なんて殆ど無いでしょう。一方、Schemeはそう言う部分は丸っきり無くって、まるでCSの宿題を解く為の言語の如しです。自分で解いて考えてくれ、と言わんばかり。



有名なのはtraceマクロでしょう(※1)。例えば再帰関数を定義して、何かおかしな結果が返るような場合、関数の挙動を逐一追ってくれたりします。




CL-USER> (defun fact (n &optional (acc 1))
(if (zerop n)
acc
(fact (1- n) (* n acc))))
FACT
CL-USER> (trace fact)
(FACT)
CL-USER> (fact 3)
0: (FACT 3)
1: (FACT 2 3)
2: (FACT 1 6)
3: (FACT 0 6)
3: FACT returned 6
2: FACT returned 6
1: FACT returned 6
0: FACT returned 6
6
CL-USER>


なお、traceを解除するにはuntraceを使います。



ちなみに、残念ながらtraceは局所関数の中身まで追っかけてきてくれません。これがSchemerとCLerのアティテュードの差をある程度生んでいて、Schemerはトップレベルでのシンボルの衝突を避ける意味もあって、ローカル手続きを多用する傾向があるんですが、CLerはSchemer程局所関数を多用せず、どちらかと言うと大域関数として補助関数を複数作る事を好みます。CLerはトップレベルのシンボルの衝突はいざとなったらパッケージで回避出来るので、デバックのやりやすさをより重要視する傾向があるんです。



また、ANSI Common Lispでは、エラーが起こるとバックトレースあるいはブレイクループと呼ばれる状態に入ります。ANSI Common Lispではこれがデバッガの基本となっていて、関数が止まった地点から逆順に何が起こったか探索していけるように設計されています。



例えばC言語を勉強したとしてもデバッギングはまた別です。「GDBの使い方」なんかを学ばないとならない。ブレークポイント設置もメンド臭いし、何よりこれは「言語の外」の話です。言語に精通したとせよ、プログラミングするには「その他のソフトウェアの使い方」を強要される。じゃないとマトモにプログラミングが出来ない。



一方、ANSI Common Lispは言語仕様の中にデバッガがある。ANSI Common Lispは一種オールインワンの言語環境を目指していて、そこに何でもあるわけです。それ以外に何も必要がない、最初からデバッガの使い方も簡単に学べる、と言う辺りがCLerの「CL最強説」の一つの源になっているのでしょう(※2)。




※1: もちろんSchemeにも実装依存でtraceを持ってる処理系があります。ただし、ここでは「実装依存がどーの」とか言うつもりは全くありません。やはりプログラミングのしやすさを考えると仕様で定義されてるべきじゃないか、と思います。逆に言うと、定義してないのは、やっぱりSchemeは宿題の為の言語なのでは、と。

なお、Gaucheで使えるtraceSLIBのものです。SRFIとはまた別の共有ライブラリなんですが、一方、PLT SchemeではSLIBは使用出来ません。

PLT Schemeでは(require mzlib/trace)を評価した後、CLと全く同じようにtraceが使えます。

Gaucheでは

(use slib)
(require 'trace)

traceが使用出来ます。

Guileではそのままデフォルトでtraceが使用可能になっています。

※2: この「ANSI Common Lispは言語環境の統合化を狙っている」ってのはマジな話で、例えばedなんて関数が仕様で定義されていて、これを走らせると「エディタを立ち上げる」事が要請されています。

Windows上のCLISPで試した事があるんですが、何と「メモ帳」が立ち上がって大爆笑した事があります(笑)。ANSI仕様では何とテキストエディタまで内包されているんです(もっとも仕様に実装依存で構わんと明記されてるんですが)。

この辺、IDEの機能であるべきものを言語仕様に含めるべきではない、と言う意見もあるとは思います。しかし、ANSI Common Lispはそれをやっちゃった。

CLerの選民思想に関する批判があって、それはその通りだと思います。ただ、敢えて言うと、「オールインワンの言語環境を提供する」ってのは別の見方をすると「ラクだ」って事でもあるんですよね。誰に対してラクか、と言われれば当然エキスパートにとって、って事もあるんですけど、プログラミング初心者にとってもラクだと言う事でもあるんです。

CLは「言語設計者にとって良い事はプログラマにとっても良い事だ」が設計思想だ、と言われてますが、同時に「プログラミング初心者にとってもラクな事はエキスパートにとってもラクだ」と言う設計思想もあるような気がします。本当はこの二つは境界線があるべきものじゃない。そしてBASICみたいにやたら初心者におもねってるわけでもないのです。

「CLerの苛立ち」と言うのは選民思想と言うよりは、

    「こんなにラクな環境を構築してるのに何で人気が無いんだろ?」

って部分に根ざしているような気がします。エラーが出たら自動的にデバッガが立ち上がったりする、ってCLの設計を見ても、どっちかと言うと、

    どんなアンポンタンでもプログラムがデッチ上げられる

ように設計されているようにしか見えない、のです。その親切設計に気づかないで

    「括弧が多いから・・・・・・。」

と文句言われたらそりゃあ頭にも来るし、引きこもるでしょう(笑)。そう言う部分はある、のです。括弧は大した問題じゃない傍流の話だから、です。

ANSI Common Lispはプログラミングのビギナーからエキスパートまでの幅広い層を対象に設計されてます。これは事実です。すべての人々のレベルに合わせて応えてくれる。

残念ながら、Common Lispの内側に住む限り最強でしょうが、90年代初頭と違ってCLの外界はどんどん様変わりしています。OSとやり取りするのが一番難しい、と言うのが現状でしょう。結局、ライブラリの問題に帰着するって現象が起きている。

Lispで書かれたOSでCommon Lispを動かす、のが実際は最強なんでしょうが、現時点では、過去のLisp OSのエミュレータか、あるいはハードウェアエミュレータくらいしか無いのが残念です。


その他細々した違い




  • Schemeの手続き/特殊形式名とANSI Common Lispの関数/特殊オペレータ名が食い違っている


    今までもちょこちょこと出てきましたが、結構名前が違うものが多いです。代表的なところで、SchemeのbeginはANSI Common Lispではprognとなっています。

  • Schemeの仕様では述語の最後は?で終わる命名規約になっている


    一方、ANSI Common Lispでは慣習的には述語はpで終わる、とされていますが、仕様内で定義されている述語を見ると、必ずしもそうじゃありません(例:atomnull)。また、Schemeでは破壊的変更を行う手続きに関しては!で終わる命名規約になっていますが、その手の命名規約はCommon Lispにはありません。これらのゴチャゴチャは、Common Lispの元となったZetalisp、MacLisp、Interlispで書かれたコードと最大限互換性を保つ為、だと思われます。要するに結構デタラメです。色んな意味で(笑)。

    なお、独自に述語を作る際、実践Common LispやLOLではCommon Lisp流の-pスタイルを使っていますが、ポール・グレアムはScheme式に?で終わらせる事を好んでる模様です。

  • ANSI Common Lispでは(if pred? then)で述語部が偽の場合、nilを返す


    Schemeではこの場合、返り値が未定義で、処理系によってはこの書き方は単にエラーになります。一方、ANSI Common Lispではどんな場合でも値を返すので素直にnilを返してくれます。

  • Schemeのcondの「その他」はelseだが、ANSI Common Lispでは単にt


    これはもうまんまそのまんま、です。

  • Schemeのcaseの「その他」はelseだが、ANSI Common Lispでは何故かotherwise


    何ででしょ?もう知らん(笑)。



ANSI Common Lispのマクロ



衛生的マクロから伝統的マクロへ




Schemeが方向を誤ったのは、マクロの構築を目的とするドメイン固有言語を推進したことである。Schemeのミニ言語は確かに強力だが、マクロの勘所を完全に外している。マクロは初心者用プリプロセッサ言語ではなく、Lispで書くから偉大なのだ。



Schemeの衛生的マクロ、特にR5RSのヤツは、パターンマッチングと言う武器を使って、分かりやすいテンプレート変換を用いた機能の提供を行っています。確かに比較的分かりやすく、敷居は低いんです。


ただし、これがLispなのか?と言うと……実際問題まるで別物です。LOLで示唆されているように、Scheme自体の文法とまるで関係なく衛生的マクロは存在している。つまり、仕様としてはSchemeはScheme+別のミニ言語を提供しているんだ、って言って良いでしょう。


とは言っても、Schemeの衛生的マクロを見た後だと、比較的ラクにANSI Common Lispのマクロの世界には入っていきやすいとは思います。何故ならどの道狙いは同じだから、です。目的はパターンの変換。そして、単にANSI Common Lispのマクロは衛生的マクロより強力だ、ってだけの話です。



例えば、letを実装しろ、って課題があったとします。教科書的にはletlambdaの構文糖衣なんで、ANSI Common Lispのマクロで単純に書けば次のようになるでしょう。




(defmacro my-let (bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


ちょっといきなりなんで見にくいとは思いますが、敢えてインデントを揃えてSchemeの衛生的マクロでの解と照らし合わせてみれば、次のように対応している事が分かるでしょう。












Schemeの衛生的マクロでの解 ANSI Common Lispのマクロでの解


(define-syntax my-let
(syntax-rules ()
((_ ((x v) ...) body ...)
((lambda (x ...)
body ...)
v ...))))

(defmacro my-let

(bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


比べてみると、


  1. Schemeの衛生的マクロの方が(syntax-rules () ...)の為に記述要素が多い。

  2. Schemeの方が明示的なパターン変換を指定する為、括弧が多い。

  3. ANSI Common Lispのdefmacroのパラメータ・リストが衛生的マクロによる記述パターンに対応している。

  4. 他はこのレベルでは構成自体は大して変わらない。




と言う事が見て取れます。概形自体は全く同じなんです。


「ANSI Common Lisp版には`とか,@とかワケの分からん記号が跋扈してるぞ…!」

と言う感想はどーでもいいです(笑)。まずはザックリと「構成自体は似てる」って感覚をまず掴む事が大事だと思います。



衛生的マクロの(syntax-rules () ...)に関して言うと、僕もこれが何の為にあるんだか、ぶっちゃけ分からないです(笑)。これしかないんだったら、明示せんでもエエんちゃうの?とか思ってるんですが(実際は、R6RSにはsyntax-caseと言うものもあり)。まあ、恐らく、Schemeのλ式による手続き定義と対応させるためだけに入ってるんでしょうけどね。












Schemeの手続き定義の形式 Schemeのマクロ定義の形式

(define 手続き名
(lambda (引数)
...))

(define-syntax マクロ名
(syntax-rules (キーワード)
...))


形式的な一貫性を保つ為なのかどうか知りませんが、それだけの為の整合性ってのは正直イラつきますね(笑)。タイプ量が増えるだけ、なんで。実際、実装によっては、define-syntax(syntax-rules () ...)を独立で提供しつつ、なおかつこの二つを統合したマクロを提供している処理系もあります(例:PLT Schemeのdefine-syntax-rule)。反面、ANSI Common Lispのマクロにはそう言うイラつき要素はありません。



いずれにせよ、最初はSchemeの衛生的マクロで鍛えた「パターン変換」を念頭に入れてマクロを記述するようにした方が早く慣れるんじゃないか、とは思います。



以下は古いLispの本の筋書きに則った展開です。



クオートとlistは違う



何を当たり前な事を、と言う話なんですけど、実は手癖で忘れる可能性が高い話なんじゃないか、と思います。



例えば、(1 2 3 4 5)と言うリスト*lst*を定義せよ、と言われたら殆どの人が




(defparameter *lst* '(1 2 3 4 5))


と書くでしょう。まあ、十中八九書きますよね。でもリストを生成する関数はlistなんで、




(defparameter *lst* (list 1 2 3 4 5))


って書いてもいい筈。でも書かない。何故なら前者の方が短くリストを設定出来るから、です。後者は長い。よって前者のパターンばっか書く確率が高いでしょう。そしてある意味、基本関数である筈のlistはもっとも使われない関数となってしまうんです。



ところが、一見似た結果をもたらす両者なんですが、次のようにしてみると当然結果が違うんです。




CL-USER> (defparameter *quoted-lst* '(1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9)))
*QUOTED-LST*
CL-USER> *quoted-lst*
(1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9))
CL-USER> (defparameter *listed-lst* (list 1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9)))
*LISTED-LST*
CL-USER> *listed-lst*
(1 5 -1 42 8/9)
CL-USER> (equal *quoted-lst* *listed-lst*)
NIL
CL-USER>


ここは普段意識しないでしょうから結構重要です。特殊オペレータquoteは含まれたS式の評価を完全に止めます。しかしながら、listは関数なんで最初に引数を評価してから作用する。従って、listの引数にS式を与えたら、それぞれの引数の計算結果が返ってくるんです。当たり前なんですがかなり重要な事です。



Lispのプログラムはリスト



さて、上で見た通り、関数listと特殊オペレータquoteの動作は丸っきり違うわけですけど、この二つを組み合わせるとS式を部分評価してS式を組み立てられる事に気づきます。例えば次のようにして。




CL-USER> (let ((x 0))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP 0) 'ZERO) ((PLUSP 0) 'POSITIVE) (T 'NEGATIVE))
CL-USER>


恣意的な例なんですが、Lispのプログラムを返してるのが分かるでしょうか。評価が成されてない、入力で書きそうなS式が返っています。しかも、必要な部分(この場合はx)が評価されて埋め込まれています。別な言い方をすると確かにコードを生成してるんです。あるいはテンプレートを生成した、と言うべきか。




CL-USER> (let ((x 1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP 1) 'ZERO) ((PLUSP 1) 'POSITIVE) (T 'NEGATIVE))
CL-USER> (let ((x -1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP -1) 'ZERO) ((PLUSP -1) 'POSITIVE) (T 'NEGATIVE))
CL-USER>


しかし、これらは評価されていません。実際に評価を下すのがLisp万能関数evalです。




CL-USER> (eval (let ((x 0))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
ZERO
CL-USER> (eval (let ((x 1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
POSITIVE
CL-USER> (eval (let ((x -1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
NEGATIVE
CL-USER>


つまり、defmacroの仕組みとは、基本的には、



  1. マクロの記述形式を引数として受け取る。

  2. それをリストで記述されたコードのテンプレートに受け渡す。

  3. 最終的にevalを適用する。



と言う事です。まあ、実際はこんなに単純に動作しているわけじゃないんでしょうが、「考え方」はこの通りですね。なお、簡単なdefmacroの実装方法に関してはOn Lispマクロのモデルに記載されています。



この考え方でifを定義すると、次のようになります。




(defmacro my-if (pred? then-clause else-clause) ;マクロの記述形式を引数で表現する
(list 'cond (list pred? then-clause) ;リストで組み立てられたコードのテンプレートを記述する
(list t else-clause)))


my-ifは次のような再帰的定義内でもキチンと動く事が分かります。




CL-USER> (defun fact (n &optional (acc 1))
(my-if (zerop n)
acc
(fact (1- n) (* n acc))))
FACT
CL-USER> (fact 3)
6
CL-USER>


バッククオートとカンマ



マクロの基本的な書き方は上の通りです。歴史的には、元々実際上のようにして書いてたらしいんですがlistlistlist、だと見づらいです。かつ書きづらい。そこで、listquoteを使う代わりに、この二つの組み合わせが行う事を別の書き方で表現する方法がバッククオート(※1)(日本語キーボードだとShift-@)とカンマなんです。












listquoteを使った評価例 バッククオートとカンマを使った評価例

CL-USER> (list 'a '(+ 1 2) (+ 3 4))
(A (+ 1 2) 7)
CL-USER>

CL-USER> `(a (+ 1 2) ,(+ 3 4))
(A (+ 1 2) 7)
CL-USER>


これらは写真で言うポジとネガの関係のようです。同じ事を書き表すのに反転しているように見える。


listの場合は引数で評価したい部分式はそのまま、評価を止めたい部分式にクオートしますが、バッククオートでは評価したい部分式にカンマを付けて、評価を止めたい部分式をそのまま記述します。いずれにせよ、結果は同じになります。



また、カンマは上のようにバッククオートの中で使われるのが前提なんで、カンマ単独で使用しようとする(※2)と、必ずエラーを返します。カンマはバッククオートの一部です(これは元々listの代用表記だと言う事を考えてみても分かるでしょう)。




;;; エラーの例
;;; 「カンマがバッククオートの中にないよ!」と文句を言ってくる
CL-USER> '(+ 1 ,(+ 2 3))
SB-INT:SIMPLE-READER-ERROR on #<SB-IMPL::STRING-INPUT-STREAM {AF98ED9}>:
comma not inside a backquote
[Condition of type SB-INT:SIMPLE-READER-ERROR]
; Evaluation aborted.
CL-USER>


そして、上のエラーの例見ても分かるでしょうが、クオートとバッククオートは紛らわしいです。間違えないようにしましょう。もう一回書きますが、日本語キーボードだとバッククオートはShift-@です。



my-iflistで組み立てられたヴァージョンとバッククオートで組み立てられたヴァージョンを並べて見てみます。












listとクオートで書いたmy-if バッククオートとカンマで書いたmy-if

(defmacro my-if (pred? then-clause else-clause)
(list 'cond (list pred? then-clause)
(list t else-clause)))

(defmacro my-if (pred? then-clause else-clause)
`(cond (,pred? ,then-clause)
(t ,else-clause)))


若干見やすくなってスッキリしている事が分かると思います。若干ですがね。




※1: あるいはそのまま逆引用符、等と呼んだりする。

なお、Schemeでは`quasiquote準引用符等と呼んで、バッククオートとは呼ばない。同じものを別の呼び方で呼ぶ。文化圏が違うのである。

加えて、quasiquoteはR5RSでも定義されているが、マクロが衛生的マクロしか定義されてないので、仕様書範囲内では何のために存在してるんだかサッパリ、である。単純にそれこそlistの代用としてしか使い道がない。仕様書の範囲内では。

※2: Emacs + SLIME(あるいはLispbox)では、REPLでカンマを丸裸で打つと、SLIMEのコマンドが列挙される。つまりSLIME上ではカンマはSLIMEの機能の呼び出しコマンドにあたる。従って、REPL上ではカンマが丸裸では使えないように設計されているので、ミスは減る。


macroexpand-1



若干と強調したのは、どのみちバッククオートがlistの代用である以上、見た目はスッキリしたとしてもlistでコードを組み立てるメンド臭さは変わらないと言う事だから、です。そして、メンドくさい、って事はいずれにせよ間違える



Schemeだったらコード記述時に間違えた場合、処理系は知らんぷりなんですけど、そこは過剰な親切設計であるANSI Common Lisp。マクロ記述用の一種のデバッガまで用意しています。macroexpand-1と言う関数がそのデバッガにあたります。




CL-USER> (macroexpand-1 '(my-if (zerop n)
acc
(fact (1- n) (* n acc))))
(COND ((ZEROP N) ACC) (T (FACT (1- N) (* N ACC))))
T
CL-USER>


上の例はmy-ifを用いて定義した階乗関数の一部分をmacroexpand-1に手渡したものです。macroexpand-1は関数なので、受け渡すフォーム(作成したマクロを使った部分コード)はクオートしなければなりません。しかし、それさえ守れば、コードが意図したように展開されたかどうか、一発で分かりますね。問題があったら修正、と言う流れです。



@valvallowさんがブログで記述していた宿題のうち、whenunlesspoppushなんかは仕様で定義されているマクロなんで、全部macroexpand-1で展開形を見てみて、カンニングする事が出来ます(笑)。




CL-USER> (macroexpand-1 '(when t 'hello))
(IF T (PROGN 'HELLO) NIL)
T
CL-USER> (macroexpand-1 '(unless t 'hello))
(IF T NIL (PROGN 'HELLO))
T
CL-USER> (macroexpand-1 '(pop stack))
(LET* ((#:NEW769 STACK))
(PROG1 (CAR #:NEW769) (SETQ #:NEW769 (CDR #:NEW769)) (SETQ STACK #:NEW769)))
T
CL-USER> (macroexpand-1 '(push 1 (car lst)))
(LET* ((#:G772 1) (#:TMP771 LST) (#:NEW770 (CONS #:G772 (CAR #:TMP771))))
(SB-KERNEL:%RPLACA #:TMP771 #:NEW770))
T
CL-USER>


#:とか言うのは変数名なんで、若干見にくいんですが、要するにxとかyとかの無意味な変数名と同じものです。他には、上の例の場合、SB-KERNEL:と記述されているのはSBCLの実装上、プリミティヴとして定義されている関数の事を表したりしてるんですが、いずれにせよ、それらの「独特の表記」さえ抜かせば、解読はさほど難儀ではないと思います。



いずれにせよ、ツマッた場合のカンニングは、学校のテストじゃご法度ですが、CLのマクロでは推奨されています。




マクロ理解のポイントは、それがどのようにして実装されているかを理解することである。実質的にマクロは式を変換する関数に過ぎない。



マクロ展開は単なるデバッグの補助手段ではなく、マクロの書き方の勉強手段でもあることを言っておきたい。Common Lispには100以上の組み込みマクロがあり、なかには大変複雑なものもある。そんなマクロの展開形を見ることで、それらがどう書かれたのかが分かることも多い。




destructuring-bind、カンマアットと&body



同じく、@valvallowさんのブログで示唆されている宿題ではforがあります。しかし、一般にforとは言っても、





からはじまって、色々な形式のforがあります。言語によって形式的には全く違う。



そこで、もっともメジャーだと思われるC言語のforのスタイルを借りてきます。C言語嫌いの僕でも一応K&Rは持っているんで(笑)、そこから形式を借りてきます。




for

for (expr1; expr2; expr3)




さいでっか(笑)。何とシンプルな(笑)。


ここで、はCommon Lispで言う本体部(body)、になるでしょうね。あとはCommon Lispの組み込みマクロであるdoを使って、次のようにしてでっち上げてみます。




(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))


これでK&Rの冒頭にある、華氏->摂氏の変換表も次のように、Cスタイルで簡単に書けますね。




CL-USER> (for ((fahr 0) (<= fahr 300) (incf fahr 20))
(format t "~A ~A~%" fahr (float (* 5/9 (- fahr 32)))))
0 -17.777779
20 -6.6666665
40 4.4444447
60 15.555555
80 26.666666
100 37.77778
120 48.88889
140 60.0
160 71.111115
180 82.22222
200 93.333336
220 104.44444
240 115.55556
260 126.666664
280 137.77777
300 148.88889
NIL
CL-USER>


さて、上のforは実は次のようにしても書けるんですが、




(defmacro for (exprs &body body)
`(do ((,@(car exprs) ,(third exprs)))
((not ,(cadr exprs)))
,@body))


好みにもよるんですが、若干見づらいですね。比べてみますか。












最初のヴァージョンのfor 次のヴァージョンのfor

(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(defmacro for (exprs &body body)
`(do ((,@(car exprs) ,(third exprs)))
((not ,(cadr exprs)))
,@body))


平たく言うと、ANSI Common LispではSchemeの衛生的マクロ程明らさまで強力なパターンマッチングの機能は無いんですが、一方、単純なパラメータの分配に関して言うと、行う事が出来ます。この機能をdestructuring-bindと呼びます。




Common Lispのdefmacroではパラメータリストは任意のリスト構造であってよい。マクロ呼び出しが展開されたとき、マクロ呼び出しの構成要素はマクロのパラメータにdestructuring-bindと同様に代入される。




「任意のリスト構造で良い」と言うのは次のような事です。つまり、最初のヴァージョンのforでは、expr1は二つの要素を持つリストだと仮定していました。よって、三つ以上の要素を持つリストがexpr1に渡されたら明らかにバグります。エラーチェックを行ってないわけです。


そこで「任意のリスト構造で良い」と言うのなら、これを避ける意味もあって、forは次のように記述しても構わない、と言う事です。




(defmacro for (((var start) expr2 expr3) &body body)
`(do ((,var ,start ,expr3))
((not ,expr2))
,@body))


もう一回並べておきますが、もうちょっとforの動作が明確になってるんじゃないか、と思います。












最初のヴァージョンのfor 最後のヴァージョンのfor

(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(defmacro for (((var start) expr2 expr3) &body body)
`(do ((,var ,start ,expr3))
((not ,expr2))
,@body))



控え目に使う限り、パラメータリストの分配は明確なコードにつながる。~(中略)~ 本体の式の前に複数の引数を取るようなマクロで便利だ。




となると、カンマアット(,@)(※)が何を行っているのか明確です。それはつまり、次のような作用がある、と言う事です。




CL-USER> `(,@(list 'var 'start))
(VAR START)
CL-USER> (let ((expr1 '(fahr 0))
(expr2 '(<= fahr 300))
(expr3 '(incf fahr 20))
(body '((format t "~A ~A~%" fahr (* 5/9 (- fahr 32))))))
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(DO ((FAHR 0 (INCF FAHR 20)))
((NOT (<= FAHR 300)))
(FORMAT T "~A ~A~%" FAHR (* 5/9 (- FAHR 32))))
CL-USER>



カンマアット(,@)はカンマの変種で、機能はカンマと同じだが違いが1点ある : 次に続く式の値をカンマのようにそのまま挿入するのでなく、カンマアットは切り張り操作を行う。つまり、1番外側の括弧を取り除いて挿入する :




残るは&bodyですが、これはレスト・パラメータ指定、つまり&restと基本的には同じです。ただし、macroexpand-1等を行った際に改行やインデントを施したプリティ・プリント(清書印字を)してくれるかどうかが差、なんですが、現時点、処理系によってはどっちでもプリティ・プリントを行ってくれる模様です。だから慣習的な意味しかない、と言えば無いでしょうね。関数を書く場合は&restを使って、マクロを書く場合は&bodyを使う、程度の差しか無いと言えば無いでしょう。


レスト・パラメータは以降の式をすべて纏めて単一リストにしちゃうので、本体の実行部を全てマクロ定義内に並べる為には一番大枠の括弧が要らなくなるので、当然カンマアットが必要になるわけです。




※: ちなみに、カンマアットが正式名称なのか、と言うとそれは分からない。CLHSを見る限り、バッククオートに付いての章しかなくて、

    カンマに続いてアットサインがある場合は…

のように記載されているので、特にハッキリとした名称は存在しない模様である。ただし、歴史的には通称カンマアットで間違いはない。

ちなみにSchemeのR5RSでは、,unquote,@unquote-splicingと言う洒落た名称が与えられている。


カンマ、カンマアットとmapcar



Schemeの衛生的マクロの場合、省略符号(...)とパターンマッチがあるお陰で、簡単なパターン記述がしばしば行えるわけですけど、ANSI Common Lispのマクロにはそう言う洒落た機能はありません。もう一回最初の例に戻りますが、












Schemeの衛生的マクロでのmy-let ANSI Common Lispのマクロでのmy-let


(define-syntax my-let
(syntax-rules ()
((_ ((x v) ...) body ...)
((lambda (x ...)
body ...)
v ...))))

(defmacro my-let

(bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


でScheme版だと省略符号で上手く回避しているパターン記述に対して、ANSI Common Lispでは与えられたリスト(この場合はbindings)の要素を操作して切り張りしているのはそれが理由です。カンマやカンマアットに続く部分は部分評価が成されるので、バッククオートで形作られたテンプレートにその評価結果をハメこんでいるわけです。


これはANSI Common Lispのマクロに於いて良く見かけるテクニックなんで覚えておいた方が良いと思います。かつ、悪名高いネストされたバッククオートと言うのもこれ絡みで良く出てくる現象です。


なお、必ずmapcarじゃないといけないのか、と言うとそう言うわけでもなくって、例えば実践Common Lispの著者であるPeter Seibelはmapcarよりloopを使うのが好みのようです。












mapcarを使ったmy-let loopを使ったmy-let

(defmacro my-let (bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


(defmacro my-let (bindings &body body)
`((lambda ,(loop for i in bindings collect (car i))
,@body)
,@(loop for i in bindings collect (cadr i))))


どちらでもお好きなように。展開形が同じなら、スタイルの違いは大した問題じゃない、と言うのがCommon Lispらしさと言えばらしさです。



二度あることは三度ある



以上がCLのマクロに付いての基本的な概観の全てです。



ところで、LOLでは次の二つの関数が紹介されています。












G-BANG-SYMBOL-PREDICATE O-BANG-SYMBOL-PREDICATE

(defun g!-symbol-p (s)
(and (symbolp s)
(> (length (symbol-name s)) 2)
(string= (symbol-name s)
"G!"
:start1 0
:end1 2)))

(defun o!-symbol-p (s)
(and (symbolp s)
(> (length (symbol-name s)) 2)
(string= (symbol-name s)
"O!"
:start1 0
:end1 2)))


実は殆ど全く同じです。こう言う場合、IDEだとコードをコピペして編集、ってのが一つのやり方なんですが、似たような関数だったらマクロを書いて関数を自動で生成しちゃうと言うのも手です。もちろん、この二つは目的を持って作られた関数なんですが、二度ある事は三度あるとも言いますしね(※)。今後a!-symbol-pなんて関数が欲しくならない、と言う保証もないんで、関数*!-symbol-pを生成するマクロを最後に作ってみましょう。




(defmacro !-symbol-p-generator (str)
;; 最初に材料の下処理をやっておく
(let ((len (1+ (length str))))
;; !が付くところの生成と関数名の生成をしておく
(let ((bang-part (string-upcase (concatenate 'string str "!"))))
(let ((func-name (intern (concatenate 'string bang-part "-SYMBOL-P"))))
;; マクロ本体
`(defun ,func-name (s)
(and (symbolp s)
(> (length (symbol-name s)) ,len)
(string= (symbol-name s)
,bang-part
:start1 0
:end1 ,len)))))))


Schemeの衛生的マクロとANSI Common Lispのマクロのおそらく最大の違いは、Common Lispではマクロのテンプレートを生成する前に下処理が出来る辺りです。これがR5RSのマクロでは出来ません。上のマクロ、!-symbol-p-generatorでは、"!"が付く部分(bang-part)と、関数名(func-name)を最初に生成しておいて、マクロのテンプレートにそれらをハメこんでいます。



注意点としては、internは大文字から成る文字列はそのまま素直にシンボルに直してくれるんですが、小文字が混在する場合、|(縦線)込みのシンボルにしてしまいます。




CL-USER> (intern "MY-SYMBOL")
MY-SYMBOL
NIL
CL-USER> (intern "My-Symbol")
|My-Symbol|
NIL
CL-USER>


ANSI Common Lispはcase-insensitiveなプログラミング言語でした。よって、internで関数名(シンボル)を生成する前に、引数として与える文字列を全て大文字にしておきましょう。



!-symbol-p-generatorは文字列を引数として取って関数"*!-symbol-p"を返すマクロです。macroexpand-1g!-symbol-po!-symbol-pが生成されてるのかどうか確認してみましょう。




CL-USER> (macroexpand-1 '(!-symbol-p-generator "g"))
(DEFUN G!-SYMBOL-P (S)
(AND (SYMBOLP S) (> (LENGTH (SYMBOL-NAME S)) 2)
(STRING= (SYMBOL-NAME S) "G!" :START1 0 :END1 2)))
T
CL-USER> (macroexpand-1 '(!-symbol-p-generator "o"))
(DEFUN O!-SYMBOL-P (S)
(AND (SYMBOLP S) (> (LENGTH (SYMBOL-NAME S)) 2)
(STRING= (SYMBOL-NAME S) "O!" :START1 0 :END1 2)))
T
CL-USER>


上手い具合にコードが生成されている模様ですね。実際動かしてみましょうか。




CL-USER> (!-symbol-p-generator "g")
G!-SYMBOL-P
CL-USER> (g!-symbol-p 'hoge)
NIL
CL-USER> (g!-symbol-p 'g!hoge)
T
CL-USER> (g!-symbol-p 'o!hoge)
NIL
CL-USER> (!-symbol-p-generator "o")
O!-SYMBOL-P
CL-USER> (o!-symbol-p 'hoge)
NIL
CL-USER> (o!-symbol-p 'g!hoge)
NIL
CL-USER> (o!-symbol-p 'o!hoge)
T
CL-USER>


もちろん、!-symbol-p-generatorは一文字以上の文字列も受け取って*!-symbol-pを生成出来ます。




CL-USER> (!-symbol-p-generator "bang")
BANG!-SYMBOL-P
CL-USER> (bang!-symbol-p 'hoge)
NIL
CL-USER> (bang!-symbol-p 'g!hoge)
NIL
CL-USER> (bang!-symbol-p 'o!hoge)
NIL
CL-USER> (bang!-symbol-p 'bang!hoge)
T
CL-USER>


こうなってくると楽しくなってきますね(笑)。




※: 個人的な経験では、小さなREPLを書いてる時良く起こります。例えばアドヴェンチャー・ゲームの入力/表示部分とか。ちょっとだけ内容が違うのに、形式的には全く同じ関数を何度も書いてる事に気づいて嫌になりました。

C言語でやると大体コードがscanf/printfまみれになって、それこそIDEでコードを大量にコピペするケースでしょうが、LispだったらマクロでREPLの概形を書き下して、マクロに関数を自動生成させた方が手っ取り早かったりします。修正するなら生成側のマクロ「だけ」を修正すれば良く、自動生成された関数は勝手に定義しなおされます。ある意味、オブジェクト指向のクラスの継承なんかより遥に強力でしょう。


Schemeの伝統的マクロ



取りあえずこれで #9LISP 014用のメモは全て、です。最後に実装依存ですが、




「やっぱSchemeがいいなあ~~。」


と言う人の為に、いくつかのScheme処理系での伝統的マクロの使い方を紹介しておきます。




  • Gauche: デフォルトでdefine-macroと言う形式で利用可能です。

  • PLT Scheme: (require mzlib/defmacro)を評価した後、define-macroと言う形式で利用可能です。

  • Guile: デフォルトでdefine-macroと言う形式で利用可能です。



この3つは形式的には全て同じなので、どれもGaucheプログラミング(立ち読み版)の示唆通り動くでしょう。




;;; PLT Scheme の場合
Welcome to MzScheme v4.2.5 [3m], Copyright (c) 2004-2010 PLT Scheme Inc.
> (require mzlib/defmacro)
> (define-macro (my-let varlst . body)
(let ((vars (map car varlst))
(exps (map cadr varlst)))
`((lambda ,vars ,@body) ,@exps)))
> (my-let ()
(+ 3 4))
7
> (my-let ((x 7)
(y 10))
(+ x y))
17
> (my-let ((x 3))
(my-let ((x 10)
(y (* x x)))
y))
9
>

;;; Guile の場合
guile> (define-macro (my-let varlst . body)
(let ((vars (map car varlst))
(exps (map cadr varlst)))
`((lambda ,vars ,@body) ,@exps)))
guile> (my-let ()
(+ 3 4))
7
guile> (my-let ((x 7)
(y 10))
(+ x y))
17
guile> (my-let ((x 3))
(my-let ((x 10))
(y (* x x)))
y)
guile> (my-let ((x 3))
(my-let ((x 10)
(y (* x x)))
y))
9
guile>


パラメータ・リストの記述方法がSchemeらしい、ですが、基本これでCommon Lisp相当のマクロを記述する事が可能です。