CLOS
CLOSとはCommon Lisp Object Systemの略らしい.OOというのはメソッドというしばしば破壊的な関数がバンバンでてきて,Lispとは相容れない世界かと思っていたのだが,そうではないようだ.
クラスはdefclassで定義する.
(defclass クラス名 スーパークラスのリスト スロットのリスト)スロットとは,多くのOOプログラミング言語でスロットと呼ばれているものに該当する.以下は二つのスロットfoo,barをもつhogeというクラスを定義している.
> (defclass hoge () (foo bar)) #<STANDARD-CLASS HOGE>そして以下はスロットpiyoを持つhogeのサブクラスfugaを定義している.
> (defclass fuga (hoge) (piyo)) #<STANDARD-CLASS FUGA>インスタンス化はmake-instanceで行う.
(make-instance クラスのシンボル)make-instanceは指定したクラスのインスタンスを返す.以下はこれをsetfで変数に括りつける.
> (setf hogeinstance (make-instance 'hoge)) #<HOGE #x20400B09> > hogeinstance #<HOGE #x20400B09> > (setf fugainstance (make-instance 'fuga)) #<FUGA #x20404A79> > fugainstance #<FUGA #x20404A79>hogeinstance,fugainstanceにそれぞれクラスhoge,fugaのインスタンスが括りつけられた.インスタンスのスロットはslot-valueで参照できる.
(slot-value インスタンス スロットのシンボル)以下は上でインスタンス化したfugaのインスタンスのスロットfoo,piyoを設定,参照している.
> (setf (slot-value fugainstance 'foo) 1) 1 > (setf (slot-value fugainstance 'piyo) 2) 2 > (slot-value fugainstance 'foo) 1 > (slot-value fugainstance 'piyo) 2上のクラス定義では,インスタンス化しただけではスロットが初期化されないので,インスタンス化後いきなりスロットを参照するとエラーとなる.
> (slot-value hogeinstance 'foo) *** - Condition of type UNBOUND-SLOT. The following restarts are available: STORE-VALUE :R1 You may input a new value for (SLOT-VALUE #FOO). USE-VALUE :R2 You may input a value to be used instead of (SLOT-VALUE # FOO). > (setf (slot-value hogeinstance 'foo) 100) 100 > (slot-value hogeinstance 'foo) 100
CLOSではメソッドはクラスとは別途定義する.
(defmethod メソッド名 ((インスタンス変数 クラス)*) 本体)以下は,二つのスロットfirstname,lastnameを持つクラスpersonのメソッドを定義している.
> (defclass person () (firstname lastname)) #<STANDARD-CLASS PERSON> > (defmethod whatisyourname ((you person)) (format t "~A ~A~%" (slot-value you 'firstname) (slot-value you 'lastname))) #<STANDARD-METHOD (#)> > (setf matsu (make-instance 'person)) #<PERSON #x203F0A55> > (setf (slot-value matsu 'firstname) 'ma) MA > (setf (slot-value matsu 'lastname) 'tsu) TSU > (whatisyourname matsu) MA TSU NIL
クラスを定義する際に,スロットにいろいろな属性を付加することができる.
- :accessor
- slot-valueとは別にスロットを参照できる関数を暗黙に定義する.
- :reader
- :accessorは読み書きのためのアクセサ関数を定義するが,:readerは読み込み専用関数を暗黙に定義する.定義された関数の引数はインスタンスである.
- :write
- :accessorは読み書きのためのアクセサ関数を定義するが,:readerは書き込み専用関数を暗黙に定義する.定義された関数の第一引数は設定値,第二引数がインスタンスである.
- :initform
- 該当スロットのデフォルト値を定義.
- :initarg
- make-instance時にスロットに初期値設定を行えるようにする.:initargの後には:で始まる識別子を指定して,make-instance時にはその識別子を介して値を設定する.
> (defclass clazz () ((slot :initarg :slotIni))) #<STANDARD-CLASS CLAZZ> > (slot-value (make-instance 'clazz :slotIni 5) 'slot) 5
- :allocation
- 引数に:classを指定すると,該当スロットはインスタンス間で共有される.つまり該当フスロットがいわゆるクラスフィールドとなる.:instanceはデフォルトで,つまりインスタンスフィールドである.
- :documentation
- スロットのドキュメント.
- :type
- スロットの型を指定.
> (defclass hogefuga () ((slotA :reader getSlotA :writer setSlotA :initform 0 :initarg :slotAIni) (classShared :allocation :class :initform 0 :accessor accessToClassShared))) ;; :initargで指定したslotAIniを使用して,slotAを999で初期化する > (setf hogefugaInstance (make-instance 'hogefuga :slotAIni 999)) > (setf hogefugaInstance2 (make-instance 'hogefuga)) ;; :readerと:writerの確認. > (getslota hogefugainstance ) 999 > (setslota 100 hogefugainstance ) 100 > (getslota hogefugainstance ) 100 ;; :allocation :classの確認. ;; hogefugainstanceとhogefugainstance2の ;; classSharedの実体は同じである. > (accesstoclassshared hogefugainstance ) 0 > (setf (accesstoclassshared hogefugainstance2 ) 99) 99 > (accesstoclassshared hogefugainstance ) 99
defclassの引数から明らかなように,CLOSでは多重継承できる.多重継承下での振舞いを理解するには,優先度を理解する必要がある.以下のような継承関係を考える.
これらのクラスを実装してみる.
サンプルを実行してみる.
こんなのを実装したコードなど見たくもないが,とにかくこの状況ではクラスGの優先度リストは以下のように作成できる([]内が優先度リスト).
$> clisp inherit.lisp ===================== method of superA method of superB =====================多重継承の際には,継承クラスの指定順には意味がある.サンプルでは,二つの親クラスsuperA,superBを二つのパターンで継承した.
(defclass classAB (superA superB) (x)) (defclass classBA (superB superA) (x))classABのメソッドmethodを呼び出すと,superAのmethodが呼び出され,classBAではsuperBのmethodが呼び出されることがわかる.CLOSでは,クラスを継承する際に,優先度というものが特定のアルゴリズムで決定される.あるクラスの祖先への系譜を優先度の順に並べた優先度リストは以下のように作成できる.
- tを上,優先度リストを調べるクラスを最下にとる.
- 深さ優先,左優先探索で祖先へ向かって親クラスを並べる.
- 親への経路が右にもある場合,その親はたどらず,別の親を辿る.
- tまで辿りついたら終了.
- Gの一番左の親はC([C]).
- Cの親はAだが,Aへ至る経路が右にもあるので,探索地点をCからGに戻す([C]).
- Gの次の親はD([CD]).
- Dの親はAだが,Aへ至る経路が右にもあるので,探索地点をDからGに戻す([CD]).
- Gの次の親はE([CDE]).
- Eの親はAで,Aへ至る経路が右にはもうない([CDEA]).
- Aの親はstandard-objectだが,standard-objectへ至る経路が右にもあるので,探索地点をAからEに戻す([CDEA]).
- Eにはもう親がないので,探索地点をEからGに戻す([CDEA]).
- Gの次の親はF([CDEAF]).
- Fの親はBで,Bへ至る経路が右にはもうない([CDEAFB]).
- Bの親はstandard-objectで,standard-objectへ至る経路が右にはもうない([CDEAFB,standard-object]).
- standard-objectの親はt([CDEAFB,standard-object,t]).
(defclass superA (superS) (x)) ;(defmethod method ((x superA)) ; (format t "method of superA~%"))この状態で実行すると,
(setf instanceAB (make-instance 'classAB)) (method instanceAB)superSではなく,superBのメソッドが呼ばれる.classABではsuperSよりsuperBの方が優先度が高いからである.
$> clisp inherit.lisp ===================== method of superB method of superB =====================先のサンプル末尾のコメントアウトされた式
;(defclass classSAB (superS superA superB) (x))は,親と親の親を直接継承するクラスSABを定義しようとするものである.優先度決定アルゴリズムからして,全く意味がないことは明らかだが,そのせいか,この式を実行しようとするとエラーとなる.
$> clisp inherit.lisp ===================== method of superA method of superB ===================== *** - DEFCLASS CLASSSAB: inconsistent precedence graph, cycle (#<STANDARD-CLASS SUPERA> #
総称関数というのは,メソッドの集まりである.具体的には以下のようなことができる.
これを実行すると,以下のようになる.
(defmethod genfun ((param hoge)) (format t "param class : hoge~%")) (defmethod genfun ((param fuga)) (format t "param class : fuga~%"))メソッドの名前と引数の数が同じである.で,関数が呼ばれたときは,引数の型によってどのメソッドを実行するかが決定される.すなわち,メソッド定義での引数の優先度が,実際に指定された引数の優先度以下で最高のもの(※)を実行する.以下の例を考える.
$> clisp generic_function.lisp ================================== param class : hoge param class : fuga param class : fuga param class : fuga hoge ==================================優先度リストは(bar,foo,fuga,hoge,standard-object,t)である.特に出力から
(setf insfoo (make-instance 'foo)) (genfun insfoo)では,
(defmethod genfun ((param fuga)) (format t "param class : fuga~%"))が呼ばれている.呼んだ時の引数はクラスfooなので,fooよりも特定的な引数をとる
(defmethod genfun ((param bar)) (format t "param class : bar~%"))は除外さる.そして残った
(defmethod genfun ((param hoge)) (format t "param class : hoge~%")) (defmethod genfun ((param fuga)) (format t "param class : fuga~%"))のうち,hogeよりfugaの方が特定的なので,fuga側が呼ばれた.また,
(genfun2 insfuga insfuga)によって,
(defmethod genfun2 ((param1 hoge) (param2 fuga)) (format t "param class : hoge fuga~%")) (defmethod genfun2 ((param1 fuga) (param2 hoge)) (format t "param class : fuga hoge~%"))のうち後者が呼ばれた.引数が複数ある場合を踏まえて実行メソッド決定のルールを整理してみた.
- 全引数が特定化の範囲内である(対応する引数全てにおいて,呼び出し時の引数の優先度がメソッド定義の引数の優先度以上).このようなメソッドを適用可能なメソッドという.適用可能なメソッドがなければエラーとなる.
- 適用可能なメソッドの引数の優先度を左から順に比べる.最初により特定的なメソッドが見付かれば,それが採用される.
(defmethod genfun2 ((param1 fuga) (param2 hoge)) (format t "param class : fuga hoge~%"))が呼ばれたのである.最初の引数がhogeとfugaではfugaの方が特定的であるのでその時点で呼び出しメソッドが決定しているので,第二引数は関係ない(もちろん適用可能条件は満たす必要がある).もっとも特定的でないメソッドを定義することで,メソッドの定義洩れを防ぐことができる.
(defmethod genfun ((param)) (format t "param class : not specified~%"))こうすることで,あらゆるgenfun呼び出しの適用可能メソッドには少なくとも上のメソッドが含まれる.
※ このようなとき,「特定的」と言葉を使用することがある.「優先度が(より)高い」 = 「より特定的」.
クラスとともにスロットも継承される.このとき,スロットの属性の継承ルールは以下のようになっている.
実行結果.
- :allocation,:initform,:documentation
- 最も特定的なクラスのものを適用.
- :initarg,:accessor,:reader,:writer
- 和.継承のたびに追加的に設定されていく.
- :type
- 積.継承のたびに「かつ」な型がとられていく.
$> clisp inherit_slot.lisp initform of hoge : HOGE initform of fuga : FUGA slotA of fuga : FUGAFUGAスロットslotAに関して,:initformは最も特定的なものが採用され,:reader,:writerはクラスfugaでは両方使用できている.
今までのメソッドを基本メソッドと呼び,これに対して補助メソッドというものがある.
以下実行結果.
以下実行結果.
(defmethod メソッド名 :around ....) (defmethod メソッド名 :before ....) (defmethod メソッド名 :after ....)このように補助メソッドは基本メソッド定義のメソッド名の後に:before,:after,:aroundを記述することで定義する.
- :around
- 基本メソッドの代わりに,:aroundがついたメソッド(最も特定的なもの)を呼び出す.
- :before
- :aroundなメソッドがなければ,基本メソッド呼び出しの前に,適用可能かつ:beforeなものを特定性の高いものから順に全て呼び出す.
- :after
- :aroundなメソッドがなければ,基本メソッド呼び出しの後に,適用可能かつ:afterなものを特定性の低いものから順に全て呼び出す.
$> clisp before_after_standard_method_combination.lisp ========================================= before methodA hoge methodA hoge after methodA hoge ret = RET-METHODA-HOGE ========================================= before methodA fuga before methodA hoge methodA fuga after methodA hoge after methodA fuga ret = RET-METHODA-FUGA =========================================確かに,methodA:beforeは特定性の高いものから順に,methodA:afterは特定性の低いものから順に適用可能なものは全て呼び出されている.さらにmethodA:before,methoA:afterの呼び出しにもかかわらず基本メソッドの値が返り値として使用されている点に注意.このように,methodA:beforeとmethodA:afterは,基本メソッドを補助するように基本メソッド呼び出しの前後で呼び出される.上のサンプルにさらに:around補助メソッドを追加してみる.
$> clisp around_standard_method_combination.lisp ========================================= around methodA hoge ret = RET-AROUND-METHODA-HOGE ========================================= around methodA fuga ret = RET-AROUND-METHODA-FUGA =========================================このように,methodA:aroundがある場合は,:before,:afterのみならず基本メソッドも呼び出されない.さらにmethod:aroundは最も特定的なもののみが呼び出される.サンプルのfugaのインスタンスを引数としたmethodA:around内のif式のコメントアウトを外すと以下のようになる.
$> clisp around_standard_method_combination.lisp ========================================= around methodA hoge ret = RET-AROUND-METHODA-HOGE ========================================= around methodA fuga around methodA hoge ret = RET-AROUND-METHODA-FUGA =========================================call-next-methodは「次のメソッド」を呼び出す.next-method-pは「次のメソッド」があれば真を返す.「次」というのは,次のリストの条件を順に調べて最初にマッチするものである(※).
- より特定性の低い:aroundなメソッド
- :beforeなメソッド
- 基本メソッド
- :afterなメソッド
$> clisp around_standard_method_combination.lisp ========================================= around methodA hoge before methodA hoge methodA hoge after methodA hoge ret = RET-AROUND-METHODA-HOGE ========================================= around methodA fuga around methodA hoge before methodA fuga before methodA hoge methodA fuga after methodA hoge after methodA fuga ret = RET-AROUND-METHODA-FUGA =========================================出力の後半,fugaインスタンスを引数にとるmethodA:aroundは,hogeインスタンスを引数にとるmethodA:aroundを呼び出し,そこからさらにmethodA:before呼び出しへと続いている.なお,:aroundなメソッドが呼び出された場合は,たとえそこから基本メソッドが呼び出されたとしても:aroundなメソッド自身の返り値が返っている点に注意.
※ このルールで呼び出されるメソッドの組合せを標準メソッドコンビネーションという.
標準メソッドコンビネーションに対して,オペレータメソッドコンビネーションというコンビネーションがある.標準メソッドコンビネーションでは,基本メソッドは最も特定的なもののみが呼び出され,さらに補助メソッドが呼び出された.オペレータメソッドコンビネーションでは,以下のルールでメソッドが呼び出される.
まず
- :around
- 基本メソッドの代わりに,:aroundがついたメソッド(最も特定的なもの)を呼び出す.
- 基本メソッド
- 適用可能な:aroundなメソッドがない場合,基本メソッドを特定的なものから非特定なものに順に(適用可能な範囲内で)呼び出される.各基本メソッドの呼び出し結果はdefgenericにより設定した関数で結合される.
(defgeneric methodA (x) (:method-combination list))とすることで,:aroundがない場合の複数の基本メソッド呼び出し結果の結合方法をlistに指定している.
(defgeneric メソッド名 (引数) (:method-combination オペレータ))おそらく引数で特定度を指定できる.オペレータというよりコンビネーションタイプと呼んだ方が正しいかもしれない.オペレータには以下が指定できる.
- +
- and
- append
- list
- max
- min
- nconc
- or
- progn
- standard
(defmethod methodA list ((param hoge)) "methodA hoge")といった具合である.実行してみる.
$> clisp -i operator_method_combination.lisp > (methodA (make-instance 'hoge)) ("methodA hoge") > (methodA (make-instance 'fuga)) ("methodA fuga" "methodA hoge")二回目のmethodA呼び出しの引数はfugaのインスタンスだが,返り値は
("methodA fuga" "methodA hoge")となっている.これは基本メソッドを特定的なものから非特定なものに順に(適用可能な範囲内で)呼び出し,それを関数listで結合したものである.この後
> (defmethod methodA :around ((param hoge)) "methodA:around hoge") WARNING: The generic function #<GENERIC-FUNCTION METHODA> is being modified, but has already been called. #<STANDARD-METHOD :AROUND (#<STANDARD-CLASS HOGE>)>とすると,警告がでる.methodAは既に呼び出されているが,このdefmethodによって今後は動作がかわることを警告している(たぶん普通はこんなことしない).実際
> (methodA (make-instance 'hoge)) "methodA:around hoge" > (methodA (make-instance 'fuga)) "methodA:around hoge"と,先程と動作が異なっている.
メソッドコンビネーションを一度指定してしまうと,変更できない.
> (defclass hoge () ()) #<STANDARD-CLASS HOGE> > (defclass fuga (hoge) ()) #<STANDARD-CLASS FUGA> > (defgeneric methodA (x) (:method-combination or)) #<GENERIC-FUNCTION METHODA> > (defmethod methodA or ((instance hoge)) t) #<STANDARD-METHOD OR (#<STANDARD-CLASS HOGE>)> > (defmethod methodA or ((instance fuga)) nil) #<STANDARD-METHOD OR (#<STANDARD-CLASS FUGA>)> > (methodA (make-instance 'hoge)) T > (methodA (make-instance 'fuga)) T > (defmethod methodA + ((instance hoge)) 10) *** - OR method combination, used by #<GENERIC-FUNCTION METHODA>, allows no method qualifiers except (OR :AROUND): #<STANDARD-METHOD + (#<STANDARD-CLASS HOGE>)> > (defgeneric methodA (x) (:method-combination +)) WARNING: The generic function #<GENERIC-FUNCTION METHODA> is being modified, but has already been called. *** - + method combination, used by #<GENERIC-FUNCTION METHODA>, allows no method qualifiers except (+ :AROUND): #<STANDARD-METHOD OR (#<STANDARD-CLASS FUGA>)>変更したい場合は,一度総称関数そのものを削除する必要がある.総称関数の削除にはfmakunboundを使用する.
> (fmakunbound 'methodA) METHODA > (defgeneric methodA (x) (:method-combination +)) #<GENERIC-FUNCTION METHODA> > (defmethod methodA + ((instance hoge)) 10) #<STANDARD-METHOD + (#<STANDARD-CLASS HOGE>)> > (defmethod methodA + ((instance fuga)) 1) #<STANDARD-METHOD + (#<STANDARD-CLASS FUGA>)> > (methodA (make-instance 'hoge)) 10 > (methodA (make-instance 'fuga)) 11