#:g1: Common Lispでクラスメソッド

Posted 2019-04-21 09:32:06 GMT

Common Lispにおけるクラスメソッド

SmalltalkやRubyのようにクラスメソッドがある言語のコードをCommon Lispに移植したり、参考にして書いたりしているときに、クラスメソッドに相当する挙動が欲しくなったりするのですが、メタクラスの構成が違うので、そのまま書き写しただけでは、思ったような挙動にはなりません。

Common Lispでそのまま書き下すと大抵以下のようになりますが、

(defclass foo () ())

(defclass bar (foo) ())

(defmethod zot ((c (eql (find-class 'foo)))) "zot")

(zot (find-class 'foo)) → "zot"

(zot (find-class 'bar)) !! No applicable methods

barクラスに対してはfooのクラスメソッドを起動しません。

Smalltalk/Rubyは、クラスごとにメタクラスが存在し、クラスの継承関係とメタクラスの継承関係は同様の構成になりますが、Common Lispでは、クラスメタオブジェクトのクラスをメタクラスと呼んでいるだけなので、同じメタクラスのクラスメタオブジェクト間に継承関係はありません。

Smalltalk/Ruby風のクラスメソッド的なものをどう実現するか

Common LispでもSmalltalk/Rubyのようにクラス生成時に継承関係と同じメタクラスを作ってしまうという方法が一つの解決策です。

以下のリンクは、shiroさんが以前にPython風のクラスメソッドが欲しいという質問に回答した例です

class-prototypeを使う

クラスメソッドは、

というのがメリットですが、よくよく考えてみれば、クラスの継承関係を利用できて、インスタンスを生成しなくても起動できさえすれば問題は解決とすると、クラス定義ごとに存在するclass-prototypeを利用してディスパッチすれば良さそうです。

クラスメソッドのnewのようなものは以下のように書けるでしょう。

(defmethod new ((o foo))
  (make-instance (class-of (class-prototype (class-of o)))))

(defmethod new ((name symbol)) (new (class-prototype (find-class name))))

(defclass foo () ())

(new 'foo) → #<foo 40200C8273>

(new (class-prototype (find-class 'foo))) → #<foo 40200C87EB>

上記例は大分持って回った感じですが、とりあえずclass-prototypeを経由すればOKです。

もうすこしクラスメソッド的な例を考える

もうすこしクラスメソッド的な例としてインスタンスの集合を扱う例を考えてみます。

(defclass instance-recording-class (standard-class)
  ((instance-record :initform '()
                    :accessor class-instance-record)))

(defmethod validate-superclass ((c instance-recording-class) (sc standard-class)) T)

(defmethod make-instance :around ((class instance-recording-class) &rest initargs) (let ((inst (call-next-method))) (push inst (class-instance-record class)) inst))

(defclass A ()
  ((x :initarg x))
  (:metaclass instance-recording-class))

(defclass B (A) () (:metaclass instance-recording-class))

(dotimes (i 1000) (make-instance 'B 'x (random 100)))

(length (class-instance-record (find-class 'B))) → 1000

(defun prototypep (instance)
  (eq instance (class-prototype (class-of instance))))

(deftype prototype () `(satisfies prototypep))

(defmethod all ((x A)) (check-type x prototype) (class-instance-record (class-of x)))

(defmethod all ((x symbol)) (all (class-prototype (find-class x))))

(defmethod select ((x A) (selector function)) (check-type x prototype) (loop :for i :in (class-instance-record (class-of x)) :when (funcall selector i) :collect i))

(defmethod select ((x symbol) (selector function)) (select (class-prototype (find-class x)) selector))

(length (all 'B)) → 1000

(length (select 'B (lambda (x) (evenp (slot-value x 'x))))) → 490

(loop :repeat 10 :for x :in (select 'B (lambda (x) (evenp (slot-value x 'x)))) :collect (slot-value x 'x))(66 92 38 60 74 80 10 20 76 86)

(all (make-instance 'A)) !! The value #<a 4020099793> is not of type prototype.

生成されたインスタンスとプロトタイプが混ざるという潜在的な問題はあるので、上記では、prototype型を定義して弾いてみることにしました。

プロトタイプを利用しているので、集合に対する演算の度にインスタンスが生成されて母数が変化してしまうような問題もないことが分かるかと思います。

まとめ

Smalltalkでは、クラス生成時にクラスの継承関係保持したメタクラスが生成されますが、Common Lisp(MOP)では、クラス生成時に(当然ながら)継承関係を保持したプロトタイプが生成されます。
この関係を上手く利用すれば、Common Lispでのクラスメソッド問題も解決できそうな気がしていますがどうでしょう。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus