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で括っても括らなくても、動作に特に違いが無いように見える、という疑問。