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つの疑問が残っています。

  1. g000001さんの指摘:

    defunをできるだけ先に評価しようとして、eval-whenを付けて、最も早い評価タイミングにしたとしても、コンパイル時までになりますのでやはり手遅れ、というのが起きている現象です。

が手遅れではないように見える、という疑問。

  1. load-time-valueを使う場合、eval-whenで括っても括らなくても、動作に特に違いが無いように見える、という疑問。