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*にシンボリックリンクを張っている、って事だけです。

0 件のコメント:

コメントを投稿