ついにeval-whenにたどり着く
compile-file
関数を実装して行ったら、
最後の最後にeval-when
にたどり着きました。
eval-when
は難しいですね。
この機能を調べても全然意味が分かりません。
何に使うものなのかを簡潔に書くことすら難しいです。
たぶん「compile-file
で、コンパイルだけではなく同時に実行もしたいときに使うもの」です。
知らなかったのですがcompile-file
関数はコンパイルだけじゃなく、
場合によっては実行もするんだそうです。
じゃあ何をいつ実行するんだというのを制御するのがeval-when
です。
典型的な例文を示します。
下記のファイルをコンパイルしてみましょう。
;; aaa.lisp (defun aaa () :hello) (defmacro bbb () `(format t "~A~%" ,(aaa))) (bbb)
コンパイルします。
* (compile-file #p"aaa.lisp") ★エラー、undefined function AAA
どういうことかというと、compile-file
は(defun aaa ...)
という文を
コンパイルはするものの実行はしないため、
マクロbbb
が関数aaa
を呼び出せなかったというエラーです。
sbcl
では何故かwarning
だけで済むのですが、
何をどうしたからそうなったのか良くわかりません。
(fasl
を実行するとエラーになります)
解決策は、eval-when
を用いて
コンパイル時に(defun aaa ...)
を実行させることです。
;; aaa.lisp (eval-when (:compile-toplevel :load-toplevel :execute) (defun aaa () :hello)) (defmacro bbb () `(format t "~A~%" ,(aaa))) (bbb)
コンパイルします。
* (compile-file #p"aaa.lisp")
実行します。
* (load #p"aaa.fasl") HELLO
うまく行きました。
では、ちゃんと解説していきます。
eval-when
とはspecial formであり、
下記の条件を色々と判断してコードを処理します。
- toplevelかどうか
- compile-timeかどうか
eval-when
の引数はどうか
順に説明します。
toplevelかどうか
toplevelとはLispファイルの一番左側に記載されているものです。 下記の例の場合、
(aaa) (let ((x 10)) (bbb))
(aaa)
がtoplevel、
(let ...)
がtoplevelであり、
(bbb)
はtoplevelではありません。
例外が規約で決まっており、
toplevelのprogn
, locally
, macrolet
, symbol-macrolet
は
implicit progn
部もtoplevelになります。
あとeval-when
のimplicit progn
も同じですね。
例えばこんな感じ。
(progn (defun aaa () :hello) (bbb))
(progn ...)
はtoplevel、
(defun ...)
はtoplevel、
(bbb)
はtoplevelであり、
:hello
はtoplevelではありません。
compile-timeかどうか
規約で正式に次のような名称が決まっています。
not-compile-time
モードcompile-time-too
モード
「コンパイル時は実行しない」モードと「コンパイル時も実行する」モードです。
名前の通り、not-compile-time
の時はコンパイル時には実行しません。
コンパイルだけして終わりです。
しかしcompile-time-too
の時はコンパイルをしたあと実行されます。
compile-file
関数で「実行も」するのです。
コンパイルを開始した瞬間はnot-compile-time
モードです。
どうしたらcompile-time-too
モードになるのかというと、
eval-when
の引数が切り替えのきっかけを与えます。
eval-when
の引数はどうか
まずは話の流れから、compile-time-too
モードになる場合を説明します。
not-compile-time
->compile-time-too
モードに変更するには、
eval-when
実行時に
- toplevelである
:compile-toplevel
を引数に指定する:load-toplevel
を引数に指定する
が同時に満たされている必要があります。
例えばこんな感じ。
(eval-when (:compile-toplevel :load-toplevel :execute) ...)
逆に、compile-time-too
->not-compile-time
モードに変更するには、
eval-when
実行時に
- toplevelである
:load-toplevel
だけを引数に指定する
が同時に満たされている必要があります。
つまりこんな感じ。
(eval-when (:load-toplevel) ...)
ANSI Common Lispの規約には次のような便利な表があります。
http://www.lispworks.com/documentation/HyperSpec/Body/03_bca.htm
CT LT E Mode Action New Mode ---------- Yes Yes --- --- Process compile-time-too No Yes Yes CTT Process compile-time-too No Yes Yes NCT Process not-compile-time No Yes No --- Process not-compile-time Yes No --- --- Evaluate --- No No Yes CTT Evaluate --- No No Yes NCT Discard --- No No No --- Discard ---
cltl2の方はちょっとわかりづらいので、 ANSI Common Lispの方を見た方がいいと思います。
ActionのProcessは、モードを変更してから処理することを意味します。
もしnot-compile-time
モードの時にtoplevelのeval-when
で
:compile-toplevel
と:load-toplevel
と:execute
を指定してコンパイルした場合、
- New Modeの
compile-time-too
モードに移行 - コードをコンパイルする
:compile-toplevel
指定なので実行する
という感じになります。
Evaluateは実行を意味します。
eval-when
の引数か現在のcompile-timeモードに応じて、
コンパイル時、ロード時、その他の時に実行するかどうかを決定します。
たぶんどれかは実行されるでしょう。Evaluateなので。
ひとつも実行されない状況もありますが、
それは次に説明するDiscardに該当します。
Discardは破棄です。
コンパイル時、ロード時、その他の時に実行されず、内容は捨てられてしまいます。
では、この破棄というのは一体どのフェーズからなんでしょうか?
どういうことかというと、
(eval-when () (quote aaa bbb ccc)) ;; どうみても構文エラー
みたいな文は正しく受理されるのか?
実験では受理されました。
sbcl
, clisp
, ccl
で確認しました。
つまりeval-when
のimplicit progn
部は、
read
はされますがパース処理はされないということです。
ものすごく最初の段階から削除されることが分かります。
npt
ではeval
を行う際に、一般的なコンパイラフェーズのように
字句解析、構文解析、意味解析、コード生成みたいな段取りを経ているのですが、
eval
の初段である構文解析の段階で削除されます。
一応断っておくと、字句解析はeval
ではなくread
関数です。
モードの切り替えについてもう少し
compile-time
モードの切り替えは、
toplevelじゃないと動作しないのでしょうか?
これはyesです。
下記の文を考えます。
(eval-when (:compile-toplevel :load-toplevel :execute) (eval-when (:execute) (format t "compile-time-too~%")))
外側のeval-when
にてcompile-time-too
モードに変更しています。
内側のeval-when
は、compile-time-too
モードの時だけ出力を行うというものです。
このソースをcomple-file
で実行すると、
format
文が実行されてcompile-time-too
という文字が出力されます。
では、toplevelではない場合はどうなるでしょうか。
(let () (eval-when (:compile-toplevel :load-toplevel :execute) (eval-when (:execute) (format t "compile-time-too~%"))))
comple-file
を実行すると出力はされませんでした。
つまり、toplevelじゃないのでcompile-time-too
モードに移行しなかったということです。
ではモードを切り替えた後、:execute
の判定がtoplevelに従うかどうかを見て行きます。
具体的には下記の動作がtoplevelに関係するかどうかです。
No No Yes CTT Evaluate --- No No Yes NCT Discard ---
結果は、toplevelに関係します。
これは意外でしたが、sbcl
, clisp
, ccl
ともに同じ動作になりました。
下記の文をコンパイルしてみます。
(eval-when (:load-toplevel) (eval-when (:execute) (format t "compile-time-too~%")))
not-compile-mode
移行後に:execute
のみ指定なので、
Discardとなり実行されません。
しかし、format
文がtoplevelではない場合はどうなるでしょうか。
(eval-when (:load-toplevel) (let () (eval-when (:execute) (format t "compile-time-too~%"))))
コンパイル結果は下記の通り。
compile-time-too
実行されました。
モードによる処理の判定が、toplevelを前提にしていることが分かります。
eval-when
の引数の意味
eval-when
の構文は下記の通り。
(eval-when (stuation*) ...)
stuation
は、:compile-toplevel
, :load-toplevel
, :execute
のどれかです。
:compile-toplevel
は、toplevelのフォームを
コンパイル時に実行するようにします。
:load-toplevel
は、toplevelのフォームを
load
関数で読み込むときに実行するようにします。
:execute
は、toplevelに限らずフォームを実行するようにします。
説明してきたように、
組み合わせによってコンパイル時、ロード時に実行するようになり、
compile-time
というモードも変更されたりします。
もしフォームを破棄する場合はnil
が返却されます。
eval-when
が全く現れなかった場合、
デフォルトの状態はどうなっているのでしょうか?
標準では、コンパイル時に実行されないということなので、
not-compile-time
モードのときの、
(eval-when (:load-toplevel :execute) ...)
で実行されているのだと思います。
compile-file
以外の関数について
今まではcompile-file
関数で実験してきましたが、
式の実行とコンパイルは別の関数でも行われます。
列挙すると下記の通り。
eval-loop
eval
compile
load
(テキストファイルの読み込み)
これらの機能では、eval-when
はどう動くのでしょうか?
上記4例は全て、:execute
しか見ません。
さらにtoplevelとはみなされません。
よって、:compile-toplevel
と:load-toplevel
が意味をなさなくなるのです。
toplevel扱いされないため、モード切替もありません。
:execute
だけを見て実行するか破棄するかを判断します。
eval-when
はほぼcompile-file
関数のためのモノなんですね。
ただload
に関しては、toplevelを完全に無視しているわけではないと思います。
toplevel formは、read
で読み込まれた順番に評価して実行するという決まりがあり、
load
関数でもそれを忠実に守らないとcompile-file
との実行に差異が生じてしまいます。
実際は機能しているのかもしれませんが、
それを判断できる方法が見つかりませんでした。
定義の文はどうなるのか
toplevelに記載された定義については、
何の指定をしなくてもコンパイル時にも実行される場合があります。
例えばdeftype
。
こういうのはコンパイル時にも実行しておかないと、
あとに続く文を正常に解釈できない可能性があるためです。
ではどの定義がそうなんだというと、
ちょっと簡単には説明できないので話題にはしませんが、
基本的な考えは勝手に:compile-toplevel
が付与されるということでいいと思います。