glider-gun's Blog

何か書きます

Debugging Lisp Part 3: クラスの再定義

このエントリーは、著者の許可をいただいて http://malisper.me/category/debugging-common-lisp/ のCommon Lispのデバッグに関する連載を翻訳するものです。

目次: 第1回 第2回 第3回 第4回 第5回


このエントリーは連載 Debbugging Lisp の第3回です。第1回はこちら、第2回はこちら

Common Lisp Object System (CLOS) は非常に強力です。多重継承、多重ディスパッチ、メソッドの振る舞いを拡張するさまざまな異なるやり方を提供します。その内部では、ほとんどの実装は Metaobject Protocol(MOP) を使っています。これは CLOS 自身を使って CLOS を定義するものです。 MOP では、クラスはいくつかのインスタンス変数を持ったオブジェクトとして実装されます。そのインスタンス変数としては、クラス名、スーパークラス、またそのクラスの持つインスタンス変数のリストなどがあります。前回のエントリーの point クラスを見てみましょう:

1
2
3
(defclass point ()
  ((x :accessor point-x :initarg :x :initform 0)
   (y :accessor point-y :initarg :y :initform 0)))

そして Slime インスペクタを使って point クラスオブジェクトを見てみましょう。クラスオブジェクトは関数 find-class で見つけることができます:

MOP の利点は、 CLOS の振る舞いを微調整したいときには普通のオブジェクト指向プログラミングによって行えるということです。優れた例として、任意の述語(predecated)を CLOS のディスパッチに使えるようにするfilterd-functionsライブラリがあります。MOPについてはもういいでしょう1。このエントリーでは CLOS の小さな一部分である、 update-instance-for-redefined-class についてお話しします。

update-instance-for-redefined-class は、クラスが(実行時に)再定義される度に呼ばれるメソッドです。これをオーバーライドすることでクラス再定義の際の振る舞いをカスタマイズすることが出来ます。 たとえば何かのシミュレーション中で、先ほどのpointクラスを複素数を表すのに使っていたとしましょう。シミュレーションの一部として、pointオブジェクトを変数 *location* に保存しています。

シミュレーションをプロファイリングしてみると、ボトルネックの一つが複素数同士のかけ算であることに気付きました。複素数のかけ算は極座標表示での方がずっと効率的ですから、あなたは point クラスの実装を直交座標から極座標に変えることにしました。これを(実行中に)行うためには、次のコードを走らせるだけです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defmethod update-instance-for-redefined-class :before
     ((pos point) added deleted plist &key)
  (let ((x (getf plist 'x))
        (y (getf plist 'y)))
    (setf (point-rho pos) (sqrt (+ (* x x) (* y y)))
          (point-theta pos) (atan y x))))
 
(defclass point ()
  ((rho :initform 0 :accessor point-rho)
   (theta :initform 0 :accessor point-theta)))
 
(defmethod point-x ((pos point))
  (with-slots (rho theta) pos (* rho (cos theta))))
 
(defmethod point-y ((pos point))
  (with-slots (rho theta) pos (* rho (sin theta))))

これは要するに、 update-instance-for-redefined-class を拡張して、極座標実装での値 rhotheta を直交座標実装の値から計算するものです。 update-instance-for-redefined-class を拡張した後、このコードはクラスを再定義して、その時存在する全てのインスタンスを新しいクラスのインスタンスに変換します2。 そして最後に、二つのメソッド point-xpoint-y を定義し、 point クラスの元のインターフェースを保ちます3。コードを走らせてから *location* の中身をインスペクトすると、次のようなものが見えるはずです:

*location* は依然として同一のオブジェクトを指しているものの、今やそれは極座標実装になっています! 正しく変換されたことを確かめるため、あなたはこのオブジェクトを与えて point-x 関数を呼び出し、 x 座標が同じままかどうか確かめることにしました:

驚くべきことに、クラスの実装を完全に置き換えたにもかかわらず、全てのコードは動作し続けます。もしあなたが24時間365日動き続けていなければならないサービス内でクラスの実装を変更したくて、しかもそれがたまたま Common Lisp で書かれていたなら、 update-instance を使うことを思い出してください。

原文: http://malisper.me/2015/07/22/debugging-lisp-part-3-redefining-classes/


  1. MOPについてもっと学びたい方は、"The Art of the Metaobject Protocol"を手に取ってみてください。オブジェクト指向プログラミングを作った人であるアラン・ケイは、1997年のOOPSLAでの講演でこの本を「この10年で最も良い本」だと言いました。

  2. 実際には、どの時点でupdate-instance-for-redefined-classが呼ばれるかは仕様では策定されていません。 保証されているのは、最初にインスタンス変数がアクセスされるまでに呼ばれるということだけです。

  3. 本当にインターフェースを保存するためには initialize-instance などいくつか他の関数も定義し直す必要はあります。

Comments