#:g1: MOP vs マクロ (1)

Posted 2019-01-13 21:46:09 GMT

オブジェクト指向システムを拡張する際に、痒い所に微妙に手が届かない気がするMOPと、なんでもできるけど安易なメタプログラミングも嫌だなあというマクロで使い分けに迷うことはないでしょうか。

例えばですが、defstructにはアクセサを自動で生成する機能があり、この機能については善し悪しがありますが、defclassで同様のアクセサの自動生成を実装するとします。
さて、こういう場合、MOPで実現するのが良いのか、マクロでやっつけてしまえば良いのか微妙に悩んだりしないでしょうか(自分だけ?)

defclassでアクセサの生成をするということは、定義時には名前が確定しているということで、マクロが担当するのが良い気もします。
しかしクラスに関することなので、MOPを使った方が既存の構文の枠組みで拡張できたりもしそうです。

どっちもどっちなのですが、とりあえずはマクロで書いてみました。
defclass/conc-nameは、defcassに展開されるマクロですが、:conc-nameでアクセサの接頭辞を指定できます。

(defclass/conc-name foo ()
  (x 
   y 
   (z :accessor z))
  (:conc-name foo.))

;;; マクロ展開
===>
(defclass foo ()
  ((x :accessor foo.x)
   (y :accessor foo.y)
   (z :accessor z :accessor foo.z)))

(let ((qqq (make-instance 'foo)))
  (with-slots (x y z) qqq
    (setq x 42 y 43 z 44))
  (incf (foo.z qqq))
  (list (foo.x qqq)
        (foo.y qqq)
        (foo.z qqq)))(42 43 45) 

defclass/conc-name定義

(defun canonicalize-slots (slots)
  (labels ((canonicalize-slot (slot)
             (typecase slot
               ((and symbol (not null)) (list slot))
               (T slot))))
    (mapcar #'canonicalize-slot slots)))

(defun slot-name-conc (prefix name) (let ((pkg (etypecase prefix ((or null string) *package*) (symbol (symbol-package prefix))))) (intern (concatenate 'string (string prefix) (string name)) pkg)))

(defun ->conc-name (name) (etypecase name (null "") (symbol (string name)) (string name)))

(defmacro defclass/conc-name (name superclasses slots &rest class-options) (let* ((conc-name-p nil) (conc-name (concatenate 'string (string name) "-")) (class-options (loop :for opt :in class-options :if (eq :conc-name (car opt)) :do (setq conc-name-p T conc-name (->conc-name (cadr opt))) :else :collect opt))) `(defclass ,name (,@superclasses) (,@(loop :for s :in (canonicalize-slots slots) :when conc-name-p :do (setf (cdr (last s)) (list :accessor (slot-name-conc conc-name (car s)))) :collect s)) ,@class-options)))

マクロで実装してみた感想

マクロで書いた場合ですが、今回の場合は、

等々が問題かなと感じます。

defclassのオプションを解析するのがめんどくさいのは、defclassの作法に従おうとした結果で、その方が、使い手も類推できて良かろうという判断なのですが、どうせ拡張された構文なので解析しやすそうな構成にすることも可能かなとは思います。
defstructの作法に近付けるなら、

(defclass/conc-name (foo (:conc-name foo.)) ()
  (x 
   y 
   (z :accessor z)))

のようにできるかもしれません。
問題は、defclass/conc-namedefstructの作法で書くという情報の取扱がめんどう(使う側の人が色々憶えないといけない)ということです。

利用者の負担を減らすということでは、構文乗っ取り型マクロにしてしまう手もなくはありませんが、どうなんでしょう。

(with-conc-name foo.

(defclass foo () (x y (z :accessor z))))

;; or (with-accessor-options ((:conc-name foo.) (:foo opt)) (defclass foo () (x y (z :accessor z)))) ...

この場合は、見掛け上defclassが本体に見えますがwith-conc-namedefclassフォームを引数として処理することになります。

トリッキーですが派生構文を処理する場合には、defclass/conc-nameのような新しい名前を導入せずに既存の作法を継承できるので、一番スマートな方法だったりするかもしれません。

次回はMOPの作法で考えてみます。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus