#:g1: Metaobjectでオブジェクト指向プログラミング

Posted 2013-12-07 15:00:00 GMT

(Metaobject Protocol(MOP) Advent Calendar 2013参加エントリ)

Metaobjectでオブジェクト指向プログラミング

 Metaobject Protocol(MOP) Advent Calendar 2013 8日目です。
6日目で前フリをした続きですが、オブジェクト指向システムが、オブジェクト指向プログラミングで書かれていることについて、Common Lispでコード例を示しつつ、ざっくりと説明してみたいと思います。

プロトコル(規約)をメソッドで表現 〜インスタンス生成を例に〜

 インスタンスの生成を例に説明してみたいと思いますが、インスタンスの生成を考えてみると、

等が主なところかと思います。実用的なシステムでは、これら以外にも初期化時の値のチェック等々、細々としたものも必要になるでしょう。
MOPではインスタンスの生成と、インスタンスの初期化はMetaobjectを扱うメソッドとして表現されています。これをユーザが特殊化することで色々とカスタマイズすることができます。

インスタンスの数をカウントするクラスを作ってみる

 ということで、インスタンス生成をカウントするというMOPの超定番例を元に解説してみましょう。
※Common Lispで実際に動くコードとして説明しているため、余計なコード(※印)が混っていますので適宜読み飛ばして下さい


;; ※ 前準備/AMOPコンパチライブラリの読み込み
(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload :closer-mop))

 まず、カスタマイズ用のMetaobjectを作ります。
Common Lispの場合、standard-classというクラスMetaobjectの標準(標準クラスのクラス/標準のメタクラス)が用意されているので、これを継承してcounted-classというインスタンスの数をカウントするためのスロット(メンバ変数)が付いたクラスMetaobjectを定義します。
クラスMetaobjectの定義でも通常のクラスの定義と構文は同じところがポイントですが、最初は混乱するかもしれません。


(defclass counted-class (standard-class) ;standard-classを継承
  ((count :initform 0))) ;初期値は0

;;; ※ メタクラスが異なるクラス間で継承するために必要 (defmethod c2mop:validate-superclass ((c counted-class) (sc standard-class)) T)

 カスタマイズしたクラスMetaobjectの定義ができたので、これを使ってcounted-fooというクラスを定義します。
通常は、メタクラスの指定を省略すると、standard-classが指定されるので、counted-classを指定します。


(defclass counted-foo () 
  ((x :initarg :x))
  (:metaclass counted-class))

 定義できたので実行してみましょう。


(dotimes (i 5)
  (let ((class (find-class 'counted-foo))) ; counted-fooクラスを取得
    (format T 
            "~&~A count => ~D ~%"
            (make-instance class)          ; counted-fooのインスタンスを生成
            (slot-value class 'count))))
;>>  <COUNTED-FOO {1017E522F3}> count => 0 
;>>  <COUNTED-FOO {1017E52C43}> count => 0 
;>>  <COUNTED-FOO {1017E53663}> count => 0 
;>>  <COUNTED-FOO {1017E54123}> count => 0 
;>>  <COUNTED-FOO {1017E54A73}> count => 0 
;>>  
;=>  NIL

Common Lispでは、find-classでcounted-fooクラスを取得、make-instanceでインスタンスを生成、slot-valueはインスタンスのスロットの内容を取得します。
注意点は、slot-valueは、クラスのスロットにアクセスしているところでしょうか。
countedと言いつつ数を数える方法を与えていないので、まだ数がカウントできていませんが、スロットにはアクセスできています。

インスタンス生成プロトコルのカスタマイズ

 さて、インスタンス生成時に数を数える訳ですが、やはり生成するメソッドをカスタマイズするのが良いでしょう。
CLOS MOPのインスタンス生成プロトコルでは、make-instanceがカスタマイズ用に提供されていて、動作をオーバーライドできるメソッドになっています。
make-instanceは更に下位のメソッド群を呼び出し、下位のメソッド群も同様にカスタマイズできますが、今回の用途では上層のmake-instanceで十分だと思われます。
ということで、counted-class用にmake-instanceをカスタマイズすることにします。


(defmethod make-instance ((class counted-class) &rest initargs)
  (let ((instance (apply #'call-next-method class initargs)))
    (incf (slot-value class 'count))
    instance))

make-instanceにはクラスが渡されてきますが、今回の場合、単に数を数えれば良いだけなので、call-next-methodで上位メソッドに仕事を丸投げし結果を受け取っています。
上位メソッドから返り値を貰って返す処理の間に

(incf (slot-value class 'count))
でクラスのcountスロットの値をインクリメントします。

 ちなみに、Common Lispの場合、Pythonのデコレータのような機能があるので、副作用だけが目的の場合、下記のように簡潔に書くこともできます。


(defmethod make-instance :after ((class counted-class) &key)
  (incf (slot-value class 'count)))

 では動かしてみましょう。


(dotimes (i 5)
  (let ((class (find-class 'counted-foo)))
    (format T 
            "~&~A count => ~D ~%"
            (make-instance class)
            (slot-value class 'count))))
;>>  #<COUNTED-FOO {10182C5AD3}> count => 1 
;>>  #<COUNTED-FOO {10182C7103}> count => 2 
;>>  #<COUNTED-FOO {10182C7E23}> count => 3 
;>>  #<COUNTED-FOO {10182C8A93}> count => 4 
;>>  #<COUNTED-FOO {10182C93F3}> count => 5 
;>>  
;=>  NIL

上手く動いているようです。

メタクラスとクラスとMetaobject

 上記の説明では、メタクラスとクラスとMetaobjectを区別しやすいように、状況に応じて呼び方を変えてみましたが、CLOS MOPの場合、

になります。
また、メタクラスについては、Smalltalkや、Rubyのように、クラスと並行に存在する特殊なクラスではなくて、単にクラスのクラスなので、Smalltalk方式に馴れている方はご注意を。

まとめ

 定番の解説をまた新しく書いてしまった感がありますが、なんとなくMOP的な動きが掴めたでしょうか。さらっと書いてあるけど、クラス情報はどうやって取得するのか、どこに収められているのか、どうやっていじるのか、等々、疑問に思われた方は鋭い。
それらの詳細は、次回以降のエントリーで書いてみたいと思います…。

 Metaobjectの扱いにデザインパターンを適用してみたら、なんか面白いことができるんじゃないだろうか等々、興味が出てきた方は、是非エントリーを。

comments powered by Disqus