40197


SBCLでライブラリを作っています。

原始的な例ですが、test.lispの中に

[code]

    (defparameter default-data
        #.(let ((size 10))
            (make-array size :initial-contents (alexandria:iota size))))
    
[/code]

のように書くと、コンパイルする時――例えば`(asdf:load-system :test-
system)`を実行したとき――に`#.`以降の部分を評価して、faslファイルには結果のarrayのみを埋め込んでくれる、つまり、test.faslをライブラリとしてロードする際にいちいち再計算されない、という理解をしています。

これを活用したいのですが、次の似た例ではうまくいきません。

[code]

    (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.
    
[/code]

こうなるのは、コンパイル時には`(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 (単に`#.`を使う):

[code]

    (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))
    
[/code]

コンパイル:

[code]

    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.
    
[/code]

コンパイルエラーが出る。`#.`以降を評価する時点では`defun`が評価されていないのだから、これはわかります。

date.lisp その2（`#.`を使い、`eval-when`で括る）:

[code]

    (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))
    
[/code]

コンパイル:

[code]

    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"  ; コンパイル時の時刻と同じ。
    
[/code]

SBCL(1.3.18, Win64)では`eval-
when`を付ければ手遅れではないように見えます（`#.`以降を評価する時点で、SBCLは既に`get-
date`を知っている）。これはSBCL独自の仕様ということなのでしょうか？

date.lisp その3 (`#.`ではなく`load-time-value`を使う):

[code]

    (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)))
    
[/code]

コンパイル:

[code]

    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"  ; コンパイル時の時刻ではない
    
[/code]

date.faslをロードしたときに、`get-date`が呼ばれて`date-string`の値が決定する……という動作に見えます。

date.lisp その4（`load-time-value`を使い、`eval-when`で括る）:

[code]

    (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)))
    
[/code]

コンパイル:

[code]

    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"  ; コンパイル時の時刻ではない
    
[/code]

`eval-when`を付けない場合と同じです。

`load-time-
value`の働き自体は[Hyperspec](http://clhs.lisp.se/Body/s_ld_tim.htm)を読んでなんとなく把握したのですが、g000001さんの例にeval-
whenがある理由（date.lispの例ならその３とその４の違い）がわかりません。

以上、大きくまとめて、２つの疑問が残っています。

  1. g000001さんの指摘: 

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

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

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


CC BY-SA 3.0
2017-12-11T01:52:35.493
40176
2017-12-13T06:21:43.063
2017-12-11T21:24:46.370
26585
26585
question
common-lisp lisp sbcl
Common Lispで、#.を使って値の埋め込みをしたい


まず、`#.`はリード時に作用するものなので、コンパイル時に作用するものではありません。  
リード時はコンパイル時より先に来ます。  
`test-func`をできるだけ先に評価しようとして、`eval-
when`を付けて、最も早い評価タイミングにしたとしても、コンパイル時までになりますのでやはり手遅れ、というのが起きている現象です。

通常、上記のようなコードは、ロード時(書き出したfaslを読み込むタイミング)までに値が決定していれば良いことが殆どです。  
ですので、下記のようにロード時に値が固定するような`load-time-value`というフォームを使うことが多いです。

[code]

    (eval-when (:compile-toplevel :load-toplevel)
      (defun test-func (size)
        (make-array size :initial-contents (alexandria:iota size))))
    
    (defparameter *default-data* 
      (load-time-value (test-func 10)))
    
[/code]


CC BY-SA 3.0
2017-12-11T15:51:59.383
40197
2017-12-13T06:21:43.063
2017-12-13T06:21:43.063
3510
3510
40176
answer