nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

make-instances-obsoleteは何もしてないのでは?

Common Lispの、sbclとcclだけの話になります。

ジェネリック関数make-instances-obsoleteとは、 クラスの再定義に関係するものです。

こいつがよく分からない。
たぶん、クラスを再定義したあとで、 すでに作成されているインスタンスを一斉に更新するときに使うのだと思います。
なんで「思います」なのかというと、更新されているように見えないからです。

まずは簡単なクラス再定義の例を示します。

;; クラス作成
(defclass aaa ()
  ((bbb :initarg :bbb)))

;; インスタンスの作成
(defparameter instance (make-instance 'aaa :bbb 100))

;; インスタンスの更新手順
;; bbbの値をcccにコピー
(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

;; クラスの再定義
(defclass aaa ()
  ((ccc :initarg :ccc)))

;; インスタンスにアクセス
(format t "~S~%" (slot-value instance 'ccc))
  -> 100

さて、ここで問題にしたいのは、instanceの更新が一体いつ行われるかです。
規約ではdefclassによる再定義からinstanceの読み書きが行われるまで、 いつでも良いことになっています。

でもタイミングが分からないというのは不安なので、 早々に更新してしまいたいと思うかもしれません。
そんなときに使うのがmake-instances-obsolete関数です。
この関数を用いることで、好きなタイミングで全てのインスタンスを 更新できるはずです。

実際には更新されないんですけどね。
実行例を示します。

;; クラス作成
(defclass aaa ()
  ((bbb :initarg :bbb)))

;; インスタンスの作成
(defparameter instance (make-instance 'aaa :bbb 100))

;; bbbの値をcccにコピー
(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

;; クラスの再定義
(defclass aaa ()
  ((ccc :initarg :ccc)))

;; ★早々に更新してしまおう
(make-instances-obsolete 'aaa)

;; インスタンスにアクセス
(format t "~S~%" (slot-value instance 'ccc))
  -> 100

一見するとうまく行っています。

それではmake-instances-obsoleteの実行で 本当に更新されたのかどうかを確認します。
確認の方法はupdate-instance-for-redefined-classの中と 更新されるはずの所にformat文を入れましょう。

(defclass aaa ()
  ((bbb :initarg :bbb)))

(defparameter instance (make-instance 'aaa :bbb 100))

(defmethod update-instance-for-redefined-class :before
  ((instance aaa) add del prop &rest initargs)
  (declare (ignore add del initargs))
  (format t "~&<<update-instance-for-redefined-class>>~%")  ;; ★確認
  (setf (slot-value instance 'ccc) (getf prop 'bbb)))

(defclass aaa ()
  ((ccc :initarg :ccc)))

(make-instances-obsolete 'aaa)

(format t "Hello~%")  ;; ★確認
(format t "~S~%" (slot-value instance 'ccc))

実行結果は次の通り。

Hello
<<update-instance-for-redefined-class>>
100

うん、ダメですね。
Helloの出力の後に更新されています。

本来であれば、update-instance-for-redefined-class関数は make-instances-obsoleteで実行されるべきなのですが、 どう見てもslot-value関数の中で実行されています。

もしかしてやり方が間違っているのかな。
次の方法でも無理でした。

(make-instances-obsolete
  (find-class 'aaa))

色々やってみたのですが、更新されている様子は無かったです。

なぜmake-instances-obsoleteは何もしないのでしょうか?
処理系を実装しようとしてみると分かるのですが、 変更される前のクラスが生成したインスタンスを全て更新する方法は、 たぶんメモリの全走査しかないと思います。
つまりgerbage collectionと同じようなことをしなければなりません。
そこまでする必要があるかどうかの微妙なラインです。

make-instanceを実行するたびにバッファか何かに 全てのインスタンスを保存しておけばいいのでは? と考えるかもしれませんが、インスタンスって数が非常に多いので あまり現実的ではないのだと思います。

という訳でmake-instances-obsoleteは実現不可ということにしましょう。

defclassによる再定義を何度も実行したいから、 make-instances-obsoleteupdate-instance-for-redefined-classを 繰り返し実行しようぜ! とか思わない方がいいです。

ただ、make-instances-obsolete関数が何もしないからと言って、 関数が実行されていない訳ではありません。
クラス再定義の時に実行はされているようです。
次に例を示します。

(defclass aaa ()
  ((bbb :initarg :bbb)))

;; 一つでもインスタンスを作成しないとfinalizeが行われないため
;; make-instances-obsoleteは起動しない
(make-instance 'aaa)

;; ★良くない確認方法
(defmethod make-instances-obsolete ((inst standard-class))
  (declare (ignore inst))
  (format t "~&<<make-instances-obsolete>>~%"))

(format t "Hello~%")
(defclass aaa ()
  ((ccc :initarg :ccc)))

実行結果は下記の通り

Hello
<<make-instances-obsolete>>

上記の例はmake-instances-obsoleteの標準の動作を上書きしてしまうので、 例え何もしないとしても良い実装とは言えませんね。

まとめると次のような結論が得られるのかと思います。

  • クラス再定義時にmake-instances-obsoleteは起動される
  • でもmake-instances-obsoleteは何もしない
  • インスタンスの更新時期はアクセスされたときのみ

アクセスされたときとは、具体的には下記の四つの関数のどれかが 実行されたときだと考えていいと思います。

  • slot-value
  • slot-exists-p
  • slot-boundp
  • slot-makunbound

いや、他にもアクセス関数はあるでしょ? 例えばwith-slotsとか、と思うかもしれませんし、 それは正しいのですが、たぶん他の機能は結局上記の 4つの関数に帰着するのかと思います。