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
のオプションを解析するのがめんどくさい等々が問題かなと感じます。
defclass
のオプションを解析するのがめんどくさいのは、defclass
の作法に従おうとした結果で、その方が、使い手も類推できて良かろうという判断なのですが、どうせ拡張された構文なので解析しやすそうな構成にすることも可能かなとは思います。
defstruct
の作法に近付けるなら、
(defclass/conc-name (foo (:conc-name foo.)) ()
(x
y
(z :accessor z)))
のようにできるかもしれません。
問題は、defclass/conc-name
はdefstruct
の作法で書くという情報の取扱がめんどう(使う側の人が色々憶えないといけない)ということです。
利用者の負担を減らすということでは、構文乗っ取り型マクロにしてしまう手もなくはありませんが、どうなんでしょう。
(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-name
がdefclass
フォームを引数として処理することになります。
トリッキーですが派生構文を処理する場合には、defclass/conc-name
のような新しい名前を導入せずに既存の作法を継承できるので、一番スマートな方法だったりするかもしれません。
次回はMOPの作法で考えてみます。
■
HTML generated by 3bmd in LispWorks 7.0.0