Common Lispで、#.を使って値の埋め込みをしたい
SBCLでライブラリを作っています。
原始的な例ですが、test.lispの中に
(defparameter default-data
#.(let ((size 10))
(make-array size :initial-contents (alexandria:iota size))))
のように書くと、コンパイルする時――例えば(asdf:load-system :test-system)を実行したとき――に#.以降の部分を評価して、faslファイルには結果のarrayのみを埋め込んでくれる、つまり、test.faslをライブラリとしてロードする際にいちいち再計算されない、という理解をしています。
これを活用したいのですが、次の似た例ではうまくいきません。
(defun test-func (size)
(make-array size :initial-contents (alexandria:iota size)))
(defparameter default-data #.(test-func 10))
; caught ERROR:
; READ error during COMPILE-FILE:
;
; The function TEST-SYSTEM::TEST-FUNC is undefined.
こうなるのは、コンパイル時には(defun test-func ...)がまだ評価されていないからだ、という理解でいます。しかし、実際にこういう問題をどう解決すればいいのかわかりません。
上の例ではtest-funcを(eval-when (:compile-toplevel :load-toplevel :execute) ...)で括れば解決するようですが、自分のコードではtest-funcがまた別のライブラリ内の関数を呼び……という構造になっています。それらの定義もすべてeval-whenで括ると、1/3くらいはeval-whenで括り、残りはいらない……というような見た目になります。それだったら、eval-whenが必要な関数や変数を、最初にほうにまとめて括り、残りはいらない、という風にすべきかも……でも、ライブラリ内の種々のルーチンのまとまり、つながりから考えるに、それとは別の順番にしたほうがずっとわかりやすい……いっそのこと、全部の定義をまとめてeval-whenで括る手もあるけれど、それはそれで不要な処理をしている気がするし……という風に悩んでいます。
こういったことに、もっとスマートな解決策はあるでしょうか? それとも、すべては解決できないので、上で考えた選択肢のどれかを取るしかないでしょうか?
追記
g000001さんの
defunをできるだけ先に評価しようとして、eval-whenを付けて、最も早い評価タイミングにしたとしても、コンパイル時までになりますのでやはり手遅れ、というのが起きている現象です。
という指摘が自分の経験則(?)とは違っていて不思議に思ったので、調べていたのですが、より謎が深まりました……
date.lisp その1 (単に#.を使う):
(defun get-date ()
(multiple-value-bind (second minute hour date month year)
(get-decoded-time)
(prin1 "get-date called.")
(format nil "~A/~A/~A ~A:~A:~A" year month date hour minute second)))
(defparameter date-string #.(get-date))
コンパイル:
C:\Users...> echo %date% %time% & sbcl --eval "(compile-file \"date.lisp\")"
2017/12/11 19:40:59.25
; caught ERROR:
; READ error during COMPILE-FILE:
;
; The function COMMON-LISP-USER::GET-DATE is undefined.
コンパイルエラーが出る。#.以降を評価する時点ではdefunが評価されていないのだから、これはわかります。
date.lisp その2(#.を使い、eval-whenで括る):
(eval-when (:compile-toplevel :load-toplevel)
(defun get-date ()
(multiple-value-bind (second minute hour date month year)
(get-decoded-time)
(prin1 "get-date called.")
(format nil "~A/~A/~A ~A:~A:~A" year month date hour minute second))))
(defparameter date-string #.(get-date))
コンパイル:
C:\Users...>echo %date% %time% & sbcl --eval "(compile-file \"date.lisp\")"
2017/12/11 19:47:01.35
; コンパイル中
get-date called.
; コンパイル成功
CL-USER> (load "date.fasl")
CL-USER> date-string
"2017/12/11 19:47:1" ; コンパイル時の時刻と同じ。
SBCL(1.3.18, Win64)ではeval-whenを付ければ手遅れではないように見えます(#.以降を評価する時点で、SBCLは既にget-dateを知っている)。これはSBCL独自の仕様ということなのでしょうか?
date.lisp その3 (#.ではなくload-time-valueを使う):
(defun get-date ()
(multiple-value-bind (second minute hour date month year)
(get-decoded-time)
(prin1 "get-date called.")
(format nil "~A/~A/~A ~A:~A:~A" year month date hour minute second)))
(defparameter date-string (load-time-value (get-date)))
コンパイル:
C:\Users...>echo %date% %time% & sbcl --eval "(compile-file \"date.lisp\")"
2017/12/11 19:51:45.33
; コンパイル成功
CL-USER> (load "date.fasl")
get-date called.
CL-USER> date-string
"2017/12/11 19:51:55" ; コンパイル時の時刻ではない
date.faslをロードしたときに、get-dateが呼ばれてdate-stringの値が決定する……という動作に見えます。
date.lisp その4(load-time-valueを使い、eval-whenで括る):
(eval-when (:load-toplevel :compile-toplevel)
(defun get-date ()
(multiple-value-bind (second minute hour date month year)
(get-decoded-time)
(prin1 "get-date called.")
(format nil "~A/~A/~A ~A:~A:~A" year month date hour minute second))))
(defparameter date-string (load-time-value (get-date)))
コンパイル:
C:\Users...>echo %date% %time% & sbcl --eval "(compile-file \"date.lisp\")"
2017/12/11 19:58:09.98
; コンパイル成功
CL-USER> (load "date.fasl")
get-date called.
CL-USER> date-string
"2017/12/11 19:58:14" ; コンパイル時の時刻ではない
eval-whenを付けない場合と同じです。
load-time-valueの働き自体はHyperspecを読んでなんとなく把握したのですが、g000001さんの例にeval-whenがある理由(date.lispの例ならその3とその4の違い)がわかりません。
以上、大きくまとめて、2つの疑問が残っています。
- g000001さんの指摘:
defunをできるだけ先に評価しようとして、eval-whenを付けて、最も早い評価タイミングにしたとしても、コンパイル時までになりますのでやはり手遅れ、というのが起きている現象です。
が手遅れではないように見える、という疑問。
load-time-valueを使う場合、eval-whenで括っても括らなくても、動作に特に違いが無いように見える、という疑問。