load-time-valueを作ろう!
目次
1. はじめに
load-time-value
という機能を知っていますか?
この機能の実装が死ぬほど面倒だったので、
ちゃんと記録に残そうという気持ちになりました。
もしこれから新たにCommon Lispを作りたいという人がいたら、
ぜひ参考にしてもらえたら幸いです。
そんなクレイジーな人は絶対にいないと思いますけど。
load-time-value
とは、先行して式を評価するという機能です。
special formに分類されているため、関数でもマクロでもありません。
どんな機能なのでしょうか?
とりあえず次の例文を見てみましょう。
(load-time-value 100) -> 100
引数がそのまま返却されています。
もうちょっと難しいのを。
(load-time-value (+ 10 20 30)) -> 60
これを見てどう思います?
だから何なんだと思いませんか?
load-time-value
は、何のためにあるのかわからない機能です。
ただ値を返すだけでしょ?
ということでnptでも適当に作ったのですが、
よく調べてみると非常に複雑なものでした。
一見簡単そうだけど実はクソみたいなやつだったと言うのは
Common Lispではよくあることで、殿堂入りがsubtypep
になります。
では一体何が複雑なのか。
どういう所で使われるのか。
これから丁寧に説明して行きます。
まず最初に、とても大切なことを言います。
『load-time-value
は、compile-file
のための機能』です。
理解するうえで重要なことなので、
まずは適当に頭の片隅にでも入れておいてください。
それでは、load-time-value
の使い方と実装方法について順に説明して行きます。
2. 使い方
まずは使い方を覚えましょう。
例文で示した通り、第一引数が実行されてその値が返却されます。
(load-time-value 100) -> 100
式は評価されて返却されます。
(load-time-value (+ 10 20 30)) -> 60
load-time-value
は、式を評価する前に先行して評価されるという機能です。
例えば次の式を考えます。
(+ (+ 10 20) (load-time-value (+ 30 40)) 50)
この式で一番最初に評価されるのは、
(+ 10 20)
ではなくload-time-value
の引数の(+ 30 40)
になります。
つまり次の式に置き換えられます。
(+ (+ 10 20) 70 50)
load-time-value
は、先行して評価するという特性上、
空のenvironment環境で評価されるという性質があります。
プロンプト上で実行した例を示します。
(defvar *aaa* 111) (let ((*aaa* 999)) (load-time-value *aaa*)) -> 111 ;; sbcl, ccl -> 999 ;; clisp
結果が分かれましたが、111
が正解になります。
(私の考え方ですので絶対ではありません)
補足しますと、clispの方もcompile-file
で実行すれば111
が返却されます。
本投稿では全然説明しませんが、 第二引数で読み込み専用にすることができます。
(load-time-value (list 10 20 30) t) -> (10 20 30)
返却されたリストが読み込み専用になります。
正確には、読み込み専用のメモリ領域に配置されるのが許されるとのこと。
なのでrplaca
なんかで変更しようとすると
たぶん何かのcondition
が発生するのか、
あるいは全く変更が反映されないか、
運が悪ければプロセスごとabortするのだと思います。
それではload-time-value
がどのように処理されるのかを説明します。
例えば次の例文を考えてみます。
(+ (load-time-value 10) (load-time-value (+ 20 30))) -> 60
この文を評価するときは、
まずload-time-value
が先行して評価されるので、
疑似的なコードに書き換えると次のようになります。
(setq gensym1 10) (setq gensym2 (+ 20 30)) (eval `(+ ,gensym1 ,gensym2)) ;; つまり(eval '(+ 10 50)) -> 60
最初にload-time-value
の式を評価して、一時変数にsetq
で代入しています。
元の式とは全く関係ない場所で実行されますので、
空のenvironmentで評価されるという制約があるのです。
これはあくまで例なので、
上記のようにコードが展開されて実行されるのではありません。
では具体的にどうなるのでしょうか?
load-time-value
の式は一体いつ評価されるのでしょうか?
read
のとき? マクロ実行時? あるいはコード実行時?
これを説明するのが大変であり、本投稿の主題ともいえる内容になります。
次の内容を思い出してください。
『load-time-value
は、compile-file
のための機能』です。
これは私が勝手に思ったことです。
しかしload-time-value
を理解するためには必要なことだと思います。
load-time-value
はcompile-file
以外でも使うことができます。
compile-file
のための機能ですが、例えばeval
でも使用できます。
でも、compile-file
のときとeval
のときでは、
実装の難易度に大きな差があります。
eval
の時の方がものすごく簡単なのです。
本投稿の主題は、compile-file
の処理でload-time-value
を
どう扱うかについて説明して行くことですが、
eval
のload-time-value
についても説明しなければなりません。
まずは先にeval
の方を説明します。
3. eval
の場合
eval
の場合はとても簡単です。
load-time-value
はマクロと同じように扱われます。
下記の式を考えます。
(+ (load-time-value 10) (load-time-value (+ 20 30)))
この場合、load-time-value
がマクロのように機能するため、
先行して10
と(+ 20 30)
が評価されます。
そして次のような式を生成します。
(+ 10 50)
この式がeval
で評価されることになります。
load-time-value
の式は、eval
の構文解析の時に評価されるため、
最終的に生成されるコードには、load-time-value
の命令が含まれません。
もう少し話題が続きます。
load
関数で読み込んだ場合はどうなるでしょうか?
答えは、一つずつread
をしてからeval
で実行するということになります。
つまり、load-time-value
という名前がついているにもかかわらず、
先に全てのload-time-value
を一斉に実行するのではありません。
次のファイルを例に挙げます。
;; aaa.lisp (format t "AAA~%") (load-time-value (format t "BBB~%")) ;; AAAより先に実行されるわけではない (format t "CCC~%") (load-time-value (format t "DDD~%"))
実行結果は下記の通り。
AAA BBB CCC DDD
何が言いたかったのかというと、 次のようにはなりませんよということです。
BBB DDD AAA CCC にはならなかった
toplevelという考え方は、実はよくわかっていません。
しかしtoplevelの式を順次実行していき、
必要に応じてload-time-value
が先行評価されるということなのでしょう。
これがeval
におけるload-time-value
の全てです。
最後にcompile
関数の話題をして終わります。
compile
関数は、eval
と全く同じように機能します。
同じコンパイルでも、compile
とcomple-file
では扱いが全く違います。
compile
とは、ファイルではなく関数であったり、
あるいはLispオブジェクトを機械語に変換する機能です。
コンパイルなのでcompile-file
と似たような感じなのかと想像するかもしれませんが、
load-time-value
に関しては、eval
の方と同じです。
compile-file
だけ特別ということなのですね。
例を示すまでもありませんが、例文を示します。
(funcall (compile nil '(lambda () (load-time-value 100)))) -> 100
4. compile-file
の場合
ここからがload-time-value
の本題となります。
eval
の場合は、マクロみたいにするだけでしたが、
compile-file
の場合はそうもいきません。
もしload-time-value
が現れたら、
その式だけを抜き出して前に持ってくる必要があります。
下記の式を考えます。
(+ (load-time-value 10) (load-time-value (+ 20 30)))
これをcompile-file
でコンパイルすることを考えます。
生成するコードの内容は次の通り。
load-time-value
用の配列を作成- 配列に
load-time-value
の評価値を代入していく - 最終的なコードを生成する
load-time-value
用の配列は、適当なspecial変数で良いと思います。
例ではload-time-value
が2つあるので、配列の長さも2
になります。
疑似コードとして次のような式が生成されます。
(defvar *load-time-value* (make-array 2))
つぎに、load-time-value
の内容を実行します。
疑似コードは次の通り。
(setf (aref *load-time-value* 0) 10) (setf (aref *load-time-value* 1) (+ 20 30))
最後に最終的なコードを生成します。
次のようなコードをコンパイルします。
(+ (aref *load-time-value* 0) (aref *load-time-value* 1)) ;; ★ダメな例
上記の式を連続で実行することで、
load-time-value
が実現できたことになります。
まとめると次の通りとなります。
;; load-time-value (defvar *load-time-value* (make-array 2)) (setf (aref *load-time-value* 0) 10) (setf (aref *load-time-value* 1) (+ 20 30)) ;; コンパイルする式 (+ (aref *load-time-value* 0) (aref *load-time-value* 1)) ;; ★ダメな例
ダメな例と記載されている通り、この方法には問題があります。
値の参照にspecial変数を使っているということで、
別の式をコンパイルすると*load-time-value*
変数が上書きされ
全く意味がなさなくなるということです。
とくにlambda式の場合なんかは、
評価する時期がわからないため悲惨な結果となります。
こんな感じ。
(lambda () (aref *load-time-value* 1)) ;; ★一体何を参照してるの?
*load-time-value*
をgensym
にするとか色々とやりようはあるのですが、
そもそもグローバルな値を参照するというやり方は
あまり良くないと思いました。
どのようなやり方がいいのでしょうか?
special変数ではなく、closureを使うという手もありました。
これはかなり自然で美しい設計だと思います。
そうしようかなとも思ったのですが、
分析が複雑なので断念して、もっと簡単な別の方法を採用しました。
代替案は次の通り。
(+ (load-time-value! 0) (load-time-value! 1))
ここで現れるload-time-value!
というのは特別な式であり、
faslファイルに出力するときに専用の命令を割り当てるというものです。
出力にはload-time-value!
に対応するバイナリコードと一緒に
番号0
と番号1
が書き込まれます。
faslファイルが生成されたあとはload
で実行できます。
ファイルを読み込むと、まずはファイルに記載されている順番で
命令コードを構築していきます。
最初は下記のコードを構築します。
(defvar *load-time-value* (make-array 2))
構築されたコードは即座に実行されます。
続いてload-time-value
フォームが読み込まれて実行されます。
(setf (aref *load-time-value* 0) 10) (setf (aref *load-time-value* 1) (+ 20 30))
問題は次の文です。
(+ (load-time-value! 0) (load-time-value! 1))
この文がfaslファイルから読み込まれ、
load-time-value!
の専用の命令がLispオブジェクトとして構築されるときに、
(load-time-value! 0)
というコードを配置するのではなく、
*load-time-value*
配列の値にそのまま置き換えます。
つまり、faslファイルから読み込んだときに値の置き換えを行うのです。
読み込まれたコードはload-time-value
などもともと無かったかのように、
次の式に相当するものとなります。
(+ 10 50)
コードが構築されたら実行します。
実行する前の段階で、special変数*load-time-value*
の役割が
終わっているのが分かるでしょうか。
生成されたコードは、何の外部変数にも依存しないものになっている必要があります。
以上で、load-time-value
の機能の説明は終わりです。
例として、toplevelに2つの式があった場合を考えます。
例文は下記の通り。
(+ (load-time-value 10) (load-time-value (+ 20 30))) (format t "~A~%" (load-time-value "Hello"))
まずはload-time-value
の数を見て配列を作成します。
(defvar *load-time-value* (make-array 3))
配列の要素数は2
ではありません。
ファイル全体の総数である3
になります。
次に、最初に式を評価します。
;; (+ (load-time-value 10) ;; (load-time-value (+ 20 30))) (setf (aref *load-time-value* 0) 10) (setf (aref *load-time-value* 1) (+ 20 30)) (+ (load-time-value! 0) (load-time-value! 1))
すでに説明した通りです。
それでは次の式を評価します。
;; (format t "~A~%" (load-time-value "Hello")) (setf (aref *load-time-value* 2) "Hello") (format t "~A~%" (load-time-value! 2))
以上でfaslファイルの生成は完了です。
load
を実行すると次の順番で式が実行されます。
10 (+ 20 30) (+ 10 50) "Hello" (format t "~A~%" "Hello")
5. make-load-form
の実行
make-load-form
をご存じでしょうか?
この機能の使い方は、以前の投稿make-load-formとは一体なんなんだ - nptclのブログで
詳しく取り上げていますので、もしよかったらどうぞ。
なんでmake-load-form
が出てきたんだというと、
生成と初期化を行う際にこれほど便利なものは無いからです。
むしろload-time-value
はmake-load-form
のためにあると言っても過言ではありません。
いや過言でした。
compile-file
の読み込み中に、CLOSオブジェクトに遭遇した場合は、
make-load-form
を実行して保存方法を決定します。
CLOSオブジェクトとはnpt用語なので、
一般的にはインスタンスと呼ばれます。
分かりづらいかもしれないですけど例を挙げます。
;; aaa.lisp (eval-when (:compile-toplevel :load-toplevel :execute) (defstruct aaa bbb ccc) (defmethod make-load-form ((x aaa) &optional env) (make-load-form-saving-slots x :environment env))) (format t "~A~%" (slot-value #s(aaa :bbb 100) 'bbb))
実行してみます。
* (compile-file #p"aaa.lisp" :output-file #p"aaa.fasl") * (load #p"aaa.fasl") 100 T *
何をしたのかというと、
(format t "~A~%" (slot-value #s(aaa :bbb 100) 'bbb))
をコンパイルして実行したということになります。
#s(aaa :bbb 100)
の部分がインスタンスであり、
make-load-form
を使ってコンパイルされた部分となります。
インスタンスに遭遇するたびにmake-load-form
が実行されるのかというとそうではなく、
以前コンパイルしたものに関しては、すでにmake-load-form
が実行されているので、
そのインデックスを参照するコードが返却されます。
つまり、コンパイルされたインスタンスは全部覚えておいて、
同一なものはちゃんとeq
で同一になるようにしましょうということです。
(eq #s(aaa :bbb 100) #s(aaa :bbb 100)) -> NIL (eq #1=#s(aaa :bbb 100) #1#) -> T
こんなふうに記載されてもよくわかりませんよね。
上記の例を疑似的にclassで出力されたかのように示すと、次のようになります。
(eq #<AAA 0x100> #<AAA 0x200>) -> NIL (eq #<AAA 0x300> #<AAA 0x300>) -> T
ではどのようにコードが生成されるのかを見て行きましょう。
前提として、defstruct
とmake-load-form
の設定はすでに済んでいるものとします。
コンパイルするコードを示します。
(eq #s(aaa :bbb 100) #s(aaa :bbb 100)) (eq #1=#s(aaa :bbb 200) #1#)
動作確認はこのようにしてください。
しかし今回は、説明を簡単にするために
下記のようなコードをコンパイルするものとします。
;; 実際にコンパイルしようとするとエラーだよ (eq #<AAA 0x100> #<AAA 0x200>) (eq #<AAA 0x300> #<AAA 0x300>)
ではコンパイルしたときの挙動を追っていきましょう。
まずはload-time-value
の数を数えます。
今回の場合は、インスタンスを数えることになります。
式の上では4
つですが、同一なものを排除すると3
つになります。
つまり生成する疑似コードは下記の通り。
(defvar *load-time-value* (make-array 3))
最初の式をコンパイルします。
make-load-form
の生成フォームを実行します。
(setf (aref *load-time-value* 0) (allocate-instance (find-class 'aaa))) (setf (aref *load-time-value* 1) (allocate-instance (find-class 'aaa)))
生成が終わったら、初期化フォームを実行します。
(setf (slot-value (load-time-value! 0) 'bbb) 100 (slot-value (load-time-value! 0) 'ccc) nil) (setf (slot-value (load-time-value! 1) 'bbb) 100 (slot-value (load-time-value! 1) 'ccc) nil)
これでインスタンスの生成と初期化は完了です。
コンパイルする式は次の通り。
(eq #<AAA 0x100> #<AAA 0x200>)
下記のように変換されます。
(eq (load-time-value! 0) (load-time-value! 1))
これで完了です。
同じようにして2つめの式をコンパイルします。
元の式を示します。
(eq #<AAA 0x300> #<AAA 0x300>)
最初の式とは違い、2つのインスタンスは同一なものなので、 生成と初期化は一度だけです。
(setf (aref *load-time-value* 2) (allocate-instance (find-class 'aaa))) (setf (slot-value (load-time-value! 2) 'bbb) 200 (slot-value (load-time-value! 2) 'ccc) nil) (eq (load-time-value! 2) (load-time-value! 2))
以上でコンパイルは完了です。
6. gensym
を扱う
なんでいきなりgensym
なんだ。
ということからちゃんと話さなければいけません。
faslファイルでgensym
の扱いをどうするかという話になります。
faslファイルというのは、compile-file
実行時とload
実行時では
Lispのheap領域が完全に別物になりますので、
eq
によるオブジェクトの同一性なんかを担保したい場合に
一体どうしたらいいんだと迷うことになります。
ひとつの解決案として導入されたのがmake-load-form
です。
これはCLOS専用ですが、いろいろと苦労しているのがわかるでしょう。
ではsymbol
の場合はどうでしょうか。
intern
されてる場合は簡単です。
だってpackage
があるのですから。
package
の名前とsymbol
の名前さえfaslに書きこんでおけば、
読み込んだときにintern
できます。
じゃあgensym
はどうするんだという話になります。
いろんな方法があると思います。
nptでは専用のhash-table
を使用していました。
でも、よく考えればload-time-value
の機能が使えるんじゃないか?
ということでこんな章が設けられたわけです。
この話題は、ちょっと複雑です。
技術的にどうこうではなく、歴史的なものが含まれるのではないかと思います。
まずは規約上どうなっているのかを説明します。
もしコンパイルされるファイルにgensym
がある場合は、
「symbol-name
が同じものは同一のオブジェクトであるとみなす」ことが許されています。
どういうことかわかりますか?
次の式が許されると言っています。
(eq '#.(gensym 10) '#.(gensym 10)) -> T (eq '#.(make-symbol "HELLO") '#.(make-symbol "HELLO")) -> T
いやいやT
の訳ないじゃないですか。
gensym
はいつだってfresh
なsymbol
のはずですよ?
これ本当にあってるのかなあ。
sbcl, clisp, cclではどれもnil
なんですよね。
私の勘違いかもしれませんが話を進めます。
大昔はgensym
の扱いに困っていたのではないでしょうか。
本来であれば、gensym
の同一判定はeq
で行う必要があります。
でも、そんな面倒なことやってられるかよ!
みたいな考え方だったのかもしれません、妄想ですが。
でもCLOS機能が実装され、make-load-form
ができると、
結局オブジェクトのeq
判定を行う必要が生じました。
何でこの時にgensym
をeq
判定にしなかったんでしょうね。
話しを戻しますが、私の考えでは、gensym
をsymbol-name
で判定するのはダメだと思います。
オブジェクトごとにeq
でちゃんと判定して分けましょう。
この動作は規約に違反しているわけではありませんが、
準拠もしていない挙動になります。
でも絶対にeq
判定の方がいいでしょう?
symbol-name
で判定されると、バグが混在する可能性が出てきます。
しかもこの手のgensym
はマクロで多用されるものなので、
せっかくバグ回避のためにgensym
を使ったのに
まだ不十分だったということが起こりえます。
On Lispでは次のような話題がありました。
「バグのないプログラムが書けるのに、どうして少々のバグを持ったプログラムを書くのですか?」
同じような理由で、symbol-name
判定ではなくeq
判定をするのが望ましいと考えます。
eq
判定にすればバグがない処理系を実装できるのに、
どうしてsymbol-name
で判定するのですか?
gensym
の実装は、make-load-form
とほぼ同じです。
gensym
が出現した時点で、CLOSと同じようにeq
による判定を行います。
生成するload-time-value
の式は下記の通り。
- 生成式は
(make-symbol name)
- 初期化は無し
これだけでgensym
の実装は完璧です。
あとは式にあるgensym
をload-time-value!
のコードに置き換えるだけです。
例文を示します。
下記の式をコンパイルします。
(eq #:G1 #:G2)
結果は下記の通り。
(defvar *load-time-value* (make-array 2)) (setf (aref *load-time-value* 0) (make-symbol "G1")) (setf (aref *load-time-value* 1) (make-symbol "G2")) (eq (load-time-value! 0) (load-time-value! 1))
なお、gensym
を生成するコードは、何よりも先に実行してください。
load-time-value
よりも、make-load-form
よりも先に実行する必要があります。
もしコンパイルするファイルに複数の式があったとしても、
一番最初にまとめてgensym
を作成してしまって問題ありません。
むしろまとめて作成したほうが安全です。
7. makefile
を使う
load-time-value
は重複が許されます。
重複とは、例を書いてしまえば次のようになります。
(load-time-value (load-time-value 10))
これが案外面倒です。
重複はこんな分かりやすいケースだけじゃないからです。
こんな場合はどうでしょうか。
(load-time-value #<AAA ...>)
load-time-value
の中にインスタンスがあります。
このインスタンスは生成と初期化の処理を持っていますので、
実質load-time-value
が2つ配置されているようなものです。
こういうケースも考えられます。
(defmethod make-load-form ((x aaa) &optional env) `(make-instance 'aaa :bbb (load-time-value 10)))
これは、生成フォームにload-time-value
が存在するケースです。
こういうのってどうしたらいいの?
生成フォームを実行する直前に
load-time-value
フォームを実行すればいいのです。
理屈ではわかっていますが、複雑な例は次々と出てきます。
規約では、生成フォーム中に別のインスタンスが現れても良いとのこと。
つまり、こんな感じ。
(defmethod make-load-form ((x aaa) &optional env) `(make-instance 'aaa :bbb #<BBB ...>))
もちろんload-time-value
フォーム中にインスタンスが現れたっていいわけです。
これもう無理ですね。
単純にこうして、ああして、みたいな決め方ではなく、
依存関係をちゃんと考える必要がありそうです。
でもどうやって?
色々考えたのですが、makefile
みたいなのを作って依存関係を表現しようと思います。
面倒すぎますが仕方がありません。
ただ、makefile
を分析しろと言われてもどうなんでしょう。
Common Lispではそこそこ簡単にできましたので、
ちょっとやってみましょう。
まずはmakefile
のルールを登録するコードを示します。
(defvar *makefile* nil) (defun makefile (x &rest args) (dolist (y args) (pushnew (cons x y) *makefile* :test 'equal)))
難しくはないと思います。
例えば、code1
がcode2
とcode3
に依存している場合を考えます。
makefile
では次のように記載すると思います。
code1: code2 code3
ではLispで表してみましょう。
(makefile :code1 :code2 :code3)
そのままですね。
依存関係を求めるコードを示します。
(defvar *pass*) (defvar *error*) (defun make-gather (name) (let (list) (dolist (x *makefile*) (when (eql (car x) name) (push (cdr x) list))) list)) ;; ★不完全バージョン (defun make-execute (name) (when (member name *error*) (error "The loop occurred by the ~S." name)) (unless (member name *pass*) (let ((*error* *error*)) (push name *error*) (push name *pass*) (mapc #'make-execute (make-gather name)) (format t "Make: ~A~%" name)))) (defun make (name) (let (*pass* *error*) (make-execute name)))
何をしているのかというと、*makefile*
に
依存関係を表すでっかいtree情報が格納されていますので、
それを末端から表示しているだけです。
もし依存関係にループが生じた場合は間違いなので、
チェックを入れています。
本場のmake
は、依存関係を見て更新するファイルを判別する機能です。
今やっていることとは少々違っていますが勘弁してください。
それではcode1
の依存関係を出力してみましょう。
(make :code1) Make: CODE2 Make: CODE3 Make: CODE1
これだけだとよくわからないかもしれないので、 もう少し複雑なルールを。
(makefile :code1 :code2 :code3) (makefile :code2 :code3 :instance1) (makefile :code3 :instance2)
実行結果は下記の通り。
(make :code1) Make: INSTANCE2 Make: CODE3 Make: INSTANCE1 Make: CODE2 Make: CODE1
うまく動作しています。
この結果から分かることは、code1
を実行するにあたっては、
一番最初にinstance2
を実行し、
連続してcode3
, instance1
...と順次実行する必要があることを示しています。
それでは今まで説明してきたload-time-value
の処理と
どのように結びつけるのかを考えます。
まず、compile-file
で読み込むコードが下記のような場合。
(code1) (code2) (code3)
記載されている通り、
code1
を実行した後にcode2
を実行する- その後
code3
を実行する
という処理になります。
依存関係として考えるならば、
code2
を実行するには、その前にcode1
の実行が必要code3
を実行するには、その前にcode2
の実行が必要
という意味になります。
すなわち下記の通り。
(makefile :code2 :code1) (makefile :code3 :code2)
実行は最後のコードであるcode3
から開始します。
(make :code3) Make: CODE1 Make: CODE2 Make: CODE3
次にload-time-value
を考えます。
下記のようなコードの場合。
(code1 (load-time-value 10))
code1
を実行するには、先に(load-time-value 10)
を実行する必要があります。
とりあえずload-time-value
の式を適当に:load1
という名前にします。
つまりこんな感じ。
(makefile :code1 :load1)
load-time-value
の重複も考えましょう。
(code1 (load-time-value 10)) ;; load1 (code2 (load-time-value ;; load2 (load-time-value ;; load3 20)))
つまりはこんな感じになります。
(makefile :code1 :load1) (makefile :code2 :code1 :load2) (makefile :load2 :load3)
実行してみます。
(make :code2) Make: LOAD1 Make: CODE1 Make: LOAD3 Make: LOAD2 Make: CODE2
それでは最後にインスタンスです。
インスタンスはmake-load-form
により、
生成フォームと初期化フォームが生成されるため、
これらの依存関係もちゃんと考えなければいけません。
単純に次の場合を考えます。
#<INSTANCE1 ...>
make-load-form
にて、生成make1
と初期化init1
が実行されるとします。
(makefile :instance1 :make1 :init1) (makefile :init1 :make1)
まず最初のルールは分かると思います。
instance1
を実行するには、make1
とinit1
が事前に実行される必要があるということ。
次のルールは、生成make1
した後じゃないと初期化init1
なんてできないと言っています。
実行結果は下記の通り。
(make :instance1) Make: MAKE1 Make: INIT1 Make: INSTANCE1
正しい順序で実行されるのが分かります。
make-load-form
は、気を付けなければならないことがあります。
なぜ生成フォームと初期化フォームが分かれているかというと、
インスタンス同士で相互参照できるようにするためです。
規約では明確に次のことが書かれています。
- 生成フォームでは相互参照できない
- 初期化フォームでは相互参照できる
どういうことかやってみましょう。
生成フォームの相互参照というのは、つまり次のようなことです。
(makefile :make1 :instance2) (makefile :make2 :instance1)
これはダメですよと言っています。
もう少しちゃんと書いて実行してみましょう。
(makefile :instance1 :make1 :init1) (makefile :init1 :make1) (makefile :instance2 :make2 :init2) (makefile :init2 :make2) (makefile :make1 :instance2) (makefile :make2 :instance1) (make :instance1) -> ★エラー
ここまでは正しい動作です。
問題は初期化フォームに相互参照があった場合です。
つまりこんな感じ。
;; ★エラーになってしまう (makefile :init1 :instance2) (makefile :init2 :instance1)
残念ながら、現状のコードではループが検出されてエラーです。
でもエラーになってはダメ。
規約でそう決められていますので。
じゃあどうしたらいいか。
たぶんですけど、makefile
レベルでどうにかなる問題じゃないと思います。
仕方がないので改造します。
init1
では、instance1
のループを検出しません、という命令を追加します。
次のような感じにしようと思います。
(makefile-loop :init1 :instance1)
登録するコードを示します。
(defvar *makefile-loop* nil) (defun makefile-loop (x y) (push (cons x y) *makefile-loop*))
超手抜きですがalistとして利用することを考えています。
この情報を反映するためのmake-execute
を示します。
(defun make-loop (name) (let ((cons (assoc name *makefile-loop*))) (when cons (setq *error* (remove (cdr cons) *error*))))) (defun make-execute (name) (when (member name *error*) (error "The loop occurred by the ~S." name)) (unless (member name *pass*) (let ((*error* *error*)) (make-loop name) ;; ★ここを追加 (push name *error*) (push name *pass*) (mapc #'make-execute (make-gather name)) (format t "Make: ~A~%" name))))
ではやり直してみます。
まずはinstance1
とinstance2
の依存関係を示します。
(makefile :instance1 :make1 :init1) (makefile :init1 :make1) (makefile-loop :init1 :instance1) (makefile :instance2 :make2 :init2) (makefile :init2 :make2) (makefile-loop :init2 :instance2)
つぎに、初期化フォームの相互参照を設定します。
(makefile :init1 :instance2) (makefile :init2 :instance1)
実行結果は下記の通り。
(make :instance1) Make: MAKE1 Make: MAKE2 Make: INIT2 Make: INSTANCE2 Make: INIT1 Make: INSTANCE1
正しく求めることができました。
以上で依存関係の登録については全てとなります。
本当はgensym
もあるのですが、
gensym
の生成処理に依存関係などありませんので、
コードの最初の方で一括して確保してしまいましょう。
ではこれらを複合した例文を示します。
(code1 (load-time-value #<INSTANCE1 ...>)) (code2 #<INSTANCE1 ...>)
makefile
は下記の通り。
(makefile :code1 :load1) (makefile :code2 :code1 :instance1) (makefile :load1 :instance1) (makefile :instance1 :make1 :init1) (makefile :init1 :make1) (makefile-loop :init1 :instance1)
さらにinstance1
の初期化フォームinit1
には、
instance2
が存在していることにしましょう。
(makefile :init1 :instance2) (makefile :instance2 :make2 :init2) (makefile :init2 :make2) (makefile-loop :init2 :instance2)
実行結果は下記の通り。
(make :code2) Make: MAKE1 Make: MAKE2 Make: INIT2 Make: INSTANCE2 Make: INIT1 Make: INSTANCE1 Make: LOAD1 Make: CODE1 Make: CODE2
あとはこの順番にコードを出力して行けば完了です。
ちょっと複雑すぎやしませんかね。
faslファイルを作るときって、本当にこんなことしなきゃダメなの?