Hatena::ブログ(Diary)

Common Lisp クックブック

Common Lisp クックブックをベースにしてます。

2007-12-12

[] 4.6. 修飾子とメソッド結合

はじめに一言。たいした思案もなしにメソッド結合を使うのは、手放しで多重継承を使うようなもので、あっと言う間にコードを把握できなくなります。

defmethod の完全な文法を次に示します。

  • defmethod function-name {method-qualifier}* specialized-lambda-list declaration* | documentation? form*

ここでは defmethod をいつも通りに使うか、標準メソッド結合 (standard method combination)の定義に使うかだけを見ていきます(その他のメソッド結合もありますし、自分で新しいメソッド結合を定義することもできますが、そこまでした人を見たことはありません)。標準メソッド結合では、次のキーワードのうち一つだけ指定できます::before, :after, around 。複数のメソッド修飾子を指定することはできません。修飾子のないメソッドは基本メソッド(primary method)と呼ばれます。総称関数のディスパッチの完全な手順を次に示します。:before:after メソッドは副作用としてのみ実行されます。

  1. 適用可能なメソッドを調べ、メソッド修飾子に従ってメソッドのリストを作ります。
  2. 適用可能な基本メソッドがなければエラーを発します。
  3. 引数定子の順序に従って、各リストを並び替えます。
  4. 最も特定的な(引数の値が最もマッチする引数定子を持つ) :around メソッドがあればそれを実行し、その戻り値を総称関数戻り値として返します。基本メソッドは実行されません
  5. :around メソッド内で call-next-method を呼ぶと、次点に特定的な :around メソッドが実行されます。
  6. :around メソッドが一つもないか、:around メソッドで call-next-method を呼んでも次点に特定的な :around メソッドがない場合、次の手順で処理が進められます。
    1. 特定的な順序で並び替えた、すべての :before メソッドを起動します。戻り値は無視されます。call-next-methodnext-method-p を呼ぶことはできません(呼ぶとエラーを発します)。
    2. 最も特定的な基本メソッドを実行し、その戻り値を総称関数の値とします。
    3. 基本メソッドが call-next-method を呼ぶと、次点に特定的な基本メソッドが実行されます。
    4. 基本メソッドが call-next-method を呼んでも次点に特定的な基本メソッドがない場合、エラーを発します。
    5. 基本メソッドの処理が終わったら、特定的でない順序で並び替えた、すべての :after メソッドを実行します。戻り値は無視され、call-next-methodnext-method-p を呼ぶことはできません。

複雑過ぎて頭がおかしくなると思った方、もっともです。一連の処理がたまねぎだとすると、最も外側の層に :around メソッドがあり、:before:after メソッドが中間の層にあり、基本メソッドが中心にあります。たったの三層で済んでいることに感謝しましょう。この仕様を理解するのに、:before:after メソッドを組み合わせた次の擬似コードが役に立つと思います(※Lispコードとして評価できないので注意。余計わかりにくいと思うけど…)。

; :before, :after メソッドを自主的に呼び出す spong メソッド
(defmethod spong :before-and-after (&rest args)
  (let ((before (find-method #'spong '(:before) args))
        (after  (find-method #'spong '(:after) args)))
    (when before (invoke-method before args))
    (multiple-value-prog1
        (call-next-before-and-after-method)
      (when after (invoke-method after args)))))

:after メソッドは特定的でない順序で呼ばれますが、実行中にどうやってその順序を知るのか考えてみてください。実はまったく簡単なことで、最初に呼ばれたメソッドが最も特定的だということを考えればわかります。*1

実際には、それほど複雑でもありません。次に my-describe戻り値を隠蔽する簡単な例を示します。

CL-USER 76 > (defmethod my-describe :around (self)
               (call-next-method)
               (values))
#<STANDARD-METHOD MY-DESCRIBE (:AROUND) (T) 20605A34>
 
; 戻り値がない(以前は NIL )
CL-USER 77 > (my-describe Eric)
Eric? Is that you?
 
CL-USER 78 >

もう一つ例を挙げます。CLOSmake-instance 関数は、二段階の処理を行います。新しいオブジェクトを生成し、make-instance のすべてのキーワード引数initialize-instance 総称関数に渡します。(処理系の)実装者やアプリケーション開発者は、インスタンスのスロットを初期化するための :after メソッドを定義することができます。システムが提供する基本メソッドは、(a) クラス定義で指定された :initform:initarg 、(b) make-instance から渡されたキーワード引数に従ってスロットを初期化します。他のメソッドでこの振る舞いを拡張することもできます。例えば、スロット値を求めるためにデータベースにアクセスするようなキーワードを追加するなどです。initialize-instance 総称関数が受け取るラムダリストを次に示します。

  • initialize-instance instance &rest initargs &key &allow-other-keys

練習問題

  • initialize-instance:after メソッドを追加し、すべての aardvarkイギリスケンブリッジ(Cambridge, England)から来たことにします。もう一つ、次の関係が成り立たなくなるようなメソッド(どんな修飾子をつけますか?)を追加しなさい。
(make-instance 'cannibal :diet (make-instance 'cannibal))
  • initialize-instance は、他のオブジェクトシステムのコンストラクタを強化したものと見ることもできます。ただし、CLOSにはデストラクタがありません。デストラクタがないのは問題ですか?

*1:実行したメソッドの :after メソッドを逆順に並び替えれば、特定的でない順に :after メソッドが並びます。

ふぇじょーーーあwwwwふぇじょーーーあwwww 2009/06/14 20:41
これヤった後でパチ屋に行ったら勝率上がりすぎwwwwww

http://shiofuki.navi-y.net/4jZ3o8C/

ただの軍資金稼ぎのつもりでヤってたんだけど、
パチも負けねーもんだから金が余りまくりっす・・(^^;
まー金は余っても困らないからまだ続けるけどねーヽ( ・∀・)ノ
とりあえずBMWでも買うわwwwwwww

しゅおおぁあああ!!!!!しゅおおぁあああ!!!!! 2009/06/22 06:41
あっもう!ちょ!!!凄い!!!凄いよ!!!!!
あぁぁテンション上がりすぎて何から言えばいいかわかんねwww
勃.起おさまんねーし今からもっかい行ってくるwwwwwww

http://ahan.yumenokuni.net/nTWo9Ho/

ぎょはぁ!!!!!ぎょはぁ!!!!! 2009/08/16 08:57
ヘイヘイ!!あひひひほはぁwwwwwww ちょwwいきなりごめwwwwww
寝てるだけで5 万もらっちゃって真面目な自分がヴァカらしくなってさwwwww
はぁーいま女シャワー浴びてんだけど、もう1ラウンドでまた5 万くれるってYO!wwwwww
またマグロでさっさと中 出 しするわwwwwwwwww

http://kachi.strowcrue.net/YEYS6SO/

2007-12-09

[] 4.5. その他の特定子(まだCLOSオブジェクトは不要です)

これまでに挙げたメソッドの例では、すべて standard-class クラスのサブクラスを特定子としていました。しかしそうでなくても、どんなCLOSクラスでも特定化することができます。例えば3.4節で挙げたクラスや、構造体クラスです。

; 構造体クラスを特定子とする
CL-USER 69 > (defmethod my-describe ((self structure-object))
               (format t "~s is a structure object."
                       self))
#<STANDARD-METHOD MY-DESCRIBE NIL (STRUCTURE-OBJECT) 205F5744>
 
; foo は構造体クラスのインスタンス
CL-USER 70 > (my-describe (make-foo))
#S(FOO) is a structure object.
NIL
 
; foo クラス(構造体クラスのサブクラス)を特定子とする
CL-USER 71 > (defmethod my-describe ((self foo))
               (format t "bar"))
#<STANDARD-METHOD MY-DESCRIBE NIL (FOO) 205F3ADC>
 
CL-USER 72 > (my-describe (make-foo))
bar
NIL
 
CL-USER 73 >

CLOSクラスをまったく定義しなくてもメソッドを使えますし、メソッドをまったく定義しなくてもCLOSクラスを使えます。CLOSの二つの要素は独立していて、一度の買い物で二つのオブジェクトシステムがついてきたようなものです。

場合によっては便利なもう一つの特定子のフォームは、eql 引数定子として知られています。次の例では、特定子のクラス名をあるリストに変更しています。このリストの最初の要素は eql シンボル、次の要素は任意のLispフォームです。このフォームは defmethod で定義したときに評価されます。メソッドを適用可能にするために、フォームの評価値と引数eql で比較されます。eql メソッドはクラスを特定子とするメソッドよりも優先されます。

; pi は float 型の定数
CL-USER 73 > (defmethod my-describe ((self (eql pi)))
               (format t "approximately 22/7"))
#<STANDARD-METHOD MY-DESCRIBE NIL ((EQL 3.141592653589793)) 2060E57C>
 
CL-USER 74 > (defmethod my-describe ((self float))
                (format t "some float"))
#<STANDARD-METHOD MY-DESCRIBE NIL (FLOAT) 2061EEF4>
 
; eql 引数特定子のメソッドが優先される
CL-USER 75 > (my-describe pi)
approximately 22/7
NIL
 
CL-USER 76 >

練習問題

  • リストのための my-describe メソッドを書きなさい。
  • antelope クラスのインスタンス Eric のための print-object メソッドを書きなさい。Eric のクラスを変更しても、そのメソッドはまだ適用可能だと思いますか?

2007-12-08

[] 4.4. すべてはオブジェクトの中にある

すべてはオブジェクトの中にある -- こんなバカバカしい思想は妄想の産物です。しかしオブジェクトシステムの多くはそう思い込もうとしていますし、理論武装もしています。CLOSオブジェクトシステムの一つとして、その考えとぶつかってみましょう。

CLOSから見た真実はこうです。アプリケーション関数とメソッド(スロットのアクセサを除く)で形作られるものであって、クラスによるものではありません。

クラスを定義したファイルに、そのクラスに適用可能なメソッドを書くこともあります。総称関数が持つすべてのメソッドを一つのファイルに書くこともあります。CLOSには言語による制限がありません。

この節のタイトルはメッセージ送信の概念とも関係があります。メッセージ送信の概念は、一つ目の引数(レシーバ)によってメソッドを決めるオブジェクトシステムから来ています。この引数が言語の特徴で、関数呼び出しの構文をその特徴に合わせると次のようになります。

Eric<-(my-describe)

これは my-describe メッセージを Eric に送ると読んでください(この例では他に引数はありません)。C++ではそのまま Eric::my_describe() と書きます。

CLOSは、複数の引数からメソッドを決定するマルチメソッドをサポートしています。あまり使うことはないかもしれませんが、他のオブジェクトシステムよりも自由というだけでなく、メッセージ送信パラダイムの制限と悪戦苦闘した犬も食わないようなコードからも解放されます。マルチメソッドの存在は、メソッドは必ずしもクラスに所属する必要がないという示唆でもあります(一つのメソッドが二つのクラスによって特定されるとしたら、どちらがメソッドの所属するクラスになるんでしょうか?)。従って、クラスを再コンパイルすることなくあなたのメソッドやその他の500のメソッドを再定義することができます。いいでしょ。

文法に関する覚え書き:マルチメソッドをサポートしない言語から拝借したと思われる、「あの」特殊な引数self と呼ぶ慣習があります。

(defmethod wibble ((self aardvark) ...) ...)

もし総称関数のメソッドが同じ引数でのみ特定されるなら、その引数を特定後のクラスの名前で呼ぶほうがましです。

(defmethod wibble ((aardvark aardvark) ...) ...)

コードは常に明確にしましょう。


練習問題

  • Lispdescribe 関数は、処理中に describe-object 総称関数を呼び出します。各処理系は各クラスにこのメソッドを実装する必要があり(ただし、standard-object 以外のクラスへの実装は任意です。また、ユーザは自由にメソッドを追加して構いません)、このメソッドがオブジェクトストリームの二つの引数を受け取るように実装しています。実装者が describe-object のすべてのメソッドを一つのファイルに書くべきか、複数のファイルに分散して書くべきか議論しなさい(例えば、aardvark で特定化されるメソッドを、クラス定義と他のメソッドと一緒に "aardvark.lisp" ファイルに書くべきかどうか)。で、どんな意見が出ましたか?
  • 二番目以降の複数の引数でメソッドを特定化しなければならない例をいくつか考えなさい。

2007-12-06

[] 4.3. 総称関数と次メソッド

総称関数は、メソッドの組み合わせと協調して動作する関数です。総称関数が起動されると、メソッドのディスパッチを行います。同じ名前を持つすべてのメソッドは、同名の総称関数に属します。

my-describe のメソッドを最初に定義したとき、同名の総称関数が暗黙のうちに生成されました。同名のメソッドを追加定義するまで、その総称関数が持つメソッドは一つだけです。

実装についての覚え書き:次の例で使っている generic-function-methods 関数method-generic-function 関数は、Common Lispの仕様ではありません。LispWorksではデフォルトのパッケージ(package-use-list 内のパッケージ)に含まれ、Allegro CLではデフォルトでロードされる ACLMOP パッケージからエクスポートされています。

; 総称関数を表示する
CL-USER 63 > #'my-describe
#<STANDARD-GENERIC-FUNCTION MY-DESCRIBE 21111C2A>
 
; 総称関数の持つメソッドを表示する
CL-USER 64 > (generic-function-methods #'my-describe)
(#<STANDARD-METHOD MY-DESCRIBE NIL (T) 2110B544>
 #<STANDARD-METHOD MY-DESCRIBE NIL (ANIMAL) 21111BF4>)
 
CL-USER 65 > (method-generic-function (car *))
#<STANDARD-GENERIC-FUNCTION MY-DESCRIBE 21111C2A>
 
CL-USER 66 >

メモ:

  • 4.2章で「メソッドの起動」について触れました。正確を期すならば、適用が直接メソッドに起動につながることはありません。関数の適用は総称関数の呼び出しにつながり、その後に最も適切なメソッドが起動されます。
  • メソッドもキーワード引数&rest 引数をとることができます。
  • 各総称関数に所属する(同名の)すべてのメソッドの引数は、完全に一致していなければいけません。例えば my-describe の二つのメソッドのうち、一方に stream オプション引数があるなら、もう一つのメソッドにもその引数がなくてはいけません。
  • defclass によって定義されるスロットアクセサとリーダは、すべてメソッドです。同名の総称関数のメソッドで再定義できます。

総称関数が起動されると、次のようにしてメソッドがディスパッチされます。

  1. 適用可能なメソッドのリストを洗い出す。
  2. 適用可能なメソッドがなければエラーにする。
  3. 定子に従って適用可能なメソッドを分類する。
  4. 最も特定度の高い(特定子に適合する)メソッドを起動する。

メソッドの実行中、残りの適用可能なメソッドに call-next-method ローカル関数でアクセスできます。この関数レキシカルスコープはメソッド本体内に限られますが、無限エクステントを持ちます(参照できる可能性がある限り存在し続ける)。この関数は次に特定度の高いメソッドを起動し、そのメソッドの戻り値を返します。この関数は次のどちらかの方法で呼び出すことができます。

  • 引数なし。次メソッドは現在のメソッドと同じ引数を受け取ります。
  • 引数あり。引数をつける場合、新しい引数に適用可能なメソッド集合の順序は、最初に総称関数が呼ばれたときのメソッド集合の順序とまったく同じでなければいけません。


訳者補足:

少々ややこしいので補足しておきます。次のコードを見てください。

(defclass animal () ())
(defclass mammal (animal) ())
(defclass human (mammal) ())
 
(defmethod my-name (any)
  (print "no name"))
 
(defmethod my-name ((animal animal))
  (print "animal"))
 
(defmethod my-name ((mammal mammal))
  (print "mammal"))
 
(defmethod my-name ((human human))
  (print "human")
  (call-next-method (make-instance 'human)))
 
(my-name (make-instance 'human))

animal, mammal, human の三つの継承関係を持つクラスと、それぞれのクラスのインスタンスに適用可能なメソッド my-name を定義しています。総称関数 my-namehumanインスタンスを渡したとき、適用可能なメソッドの特定子の順序は次のようになります。

  1. human
  2. mammal
  3. animal

human を特定子とするメソッドで、引数つきの call-next-method 関数を呼んでいます。この関数に渡す引数が、上に挙げた特定子の順序に適合しなければならないということです。上の例では humanインスタンスを渡しているので問題なく実行されますが、 mammalインスタンスを渡してみるとエラーになります。mammalインスタンスでは human定子に適合しないからです。

ここで human継承した new-human クラスを定義して、そのインスタンスcall-next-method に渡してみます。

(defclass new-human (human) ())
 
(defmethod my-name ((human human))
  (print "human")
  (call-next-method (make-instance 'new-human)))

今度はエラーが出ません。new-human は先の特定子の順序に完全に適合するからです。つまり、(この単純な例では) humanサブクラスインスタンスなら call-next-method に指定できることになります。


次メソッドがないのに call-next-method を呼ぶとエラーになります。次メソッドの有無は next-method-p ローカル関数で確認できます(この関数もメソッド本体内限定のレキシカルスコープと無限エクステントを持ちます)。

; 条件に合わなければ次メソッドを呼ぶ
CL-USER 66 > (defmethod my-describe ((antelope antelope))
               (if (string= (slot-value antelope 'comes-from)
                            "Brittany")
                   (format t "Eric? Is that you?")
                 (call-next-method)))
#<STANDARD-METHOD MY-DESCRIBE NIL (ANTELOPE) 20603594>
 
CL-USER 67 > (my-describe
              (make-instance 'antelope :comes-from 'nowhere :legs 4))
#<ANTELOPE 205ECB64> is an animal. It has 4 legs and comes from NOWHERE.
NIL
 
CL-USER 68 > (my-describe Eric)
Eric? Is that you?
NIL
 
CL-USER 69 >

すべてのメソッド本体は、メソッドの総称関数と同じ名前を持つブロックとして扱われます。return-from フォームで現在のメソッド名を指定すれば、総称関数の処理を途中で切り上げることができます。

練習問題

  • あなたが使用しているLisp処理系の、class-precedence-list の総称関数を調べなさい。
  • (comes-from Eric) を評価したとき、スロットのリーダ comes-from はどのクラスから継承されているでしょうか?そのメソッドをオーバーライドし、 antelope が常にアフリカから来ることにしなさい(現実的ではありませんが)。
  • call-next-method の無限エクステントの効果を試してみなさい。

2007-12-03

[] 4.2. defmethod マクロ

CLOSで、型を基準にした制御を定義するマクロdefmethod です。次に例を示します。

; my-describe を未束縛にする
CL-USER 57 > (fmakunbound 'my-describe)
MY-DESCRIBE
 
; my-describe を再定義する
CL-USER 58 > (defmethod my-describe (thing)
               (format t
                       "~s could be anything, for all I care."
                       thing))
#<STANDARD-METHOD MY-DESCRIBE NIL (T) 205EA9E4>
 
CL-USER 59 > (defmethod my-describe ((animal animal))
               (format t
                       "~s is an animal. It has ~d leg~:p ~
                        and comes from ~a."
                       animal
                       (leg-count animal)
                       (comes-from animal)))
#<STANDARD-METHOD MY-DESCRIBE NIL (ANIMAL) 205F476C>
  
CL-USER 60 > (my-describe Eric)
#<ANTELOPE 2112B44C> is an animal. It has 4 legs and comes from Brittany.
NIL
 
CL-USER 61 > (my-describe (make-instance 'figurine))
#<FIGURINE 205FFD14> could be anything, for all I care.
NIL
 
CL-USER 62 >

defmethod フォームは defun のように見えます。実際似ています。本体のコードと関数my-describe との組み合わせです。ただし、通常の関数ではありません。引数の型がラムダ式のパターンにマッチしたときのみ、本体が実行されます。

メソッドを起動する文法は、通常の関数を起動する文法とまったく同じです。通常の関数CLOSのメソッドの使い分けはできません(60, 61行を見てください)。メソッドは関数からでも呼べますし、その逆に関数をメソッドから呼ぶことも、双方呼び合うこともできます。

defmethod フォームを見てみましょう。パターンマッチは引数ラムダ式のリストに従って実行されます。引数リストには 変数(変数定子) のフォームを指定します。最初の例では、引数の値は通常通り変数に代入されます。しかし次の例では、引数の値は特定子のクラス(かそのサブクラス)のインスタンスである場合のみ、変数に代入されます。引数の値が特定子にマッチしなければメソッドは適用されず、その引数と共に実行されることはありません。

定子が異なるならば、同じ名前のメソッドをいくつでも定義できます。その中から、与えられたそれぞれの引数class-precedence-list に最も近い特定子を持つメソッドが特定され、本体が実行されます。

上の例では、my-describe メソッドを二度定義しました。最初のメソッドは引数の特定子を持たないので、どんな引数にも適用できます。次のメソッドは animal クラスを引数の特定子にしているので、引数animal クラスのインスタンスである場合のみ適用されます。

60行目では animal インスタンス引数として与えていますが、両方のメソッドを適用できるはずです。どうやって起動するメソッドを選ぶのでしょうか?

CL-USER 62 > (mapcar 'class-name
                     (class-precedence-list (class-of Eric)))
(ANTELOPE MAMMAL ANIMAL STANDARD-OBJECT T)
 
CL-USER 63 >

特定化されたメソッドは、その特定子引数のクラス継承リストにあれば、特定子に該当しない(もしくはデフォルトの)メソッドよりも優先されます。animalt より先にあるので、二番目に定義したメソッドが実行されることになります。言い換えると、特定化されたメソッドは該当しないメソッドを無効にするのです。

61行目では figurine インスタンス引数として与えています。figurineanimalサブクラスではありませんから、今度は適用できるメソッドが一つしかありません。

パターンマッチ処理によって二つの影響があります。

  • これまで見てきたように、値の型によってメソッドが特定されます。
  • 興味深い副作用として、特定化された値のクラスがメソッド内で保証されることになります。この副作用最適化にも有効です(例えば slot-value の呼び出しなど)。

メモ

  • 既存の関数と同名のメソッドを定義しようとするとエラーになります。エラーを避けるには、上の例のように fmakunbound を呼んで、既存の関数のシンボルを未定義にします。
  • メソッドは再定義できます(通常の関数と同じです)。
  • メソッドを定義する順序は何も影響しません。特定化されたメソッドが定義済みであっても無関係です。
  • 特定化されていない引数は、t に特定化された引数とほぼ同じです。唯一の違いは、特定化された引数は暗黙のうちに「参照される」点です(declare オプションが無視されます)。
  • defmethod フォームは standard-method クラスのインスタンスを生成し、返します。

練習問題

  • aardvarkantelope の表示を、デフォルトのものよりもわかりやすくしなさい。すべてのCLOSオブジェクトprint-object メソッドによって表示されます。このメソッドの引数(オブジェクト ストリーム) です。また、デフォルトのメソッド(standard-object を表示する)がどう定義されているのか考えなさい。
  • 次のメソッド定義を見て、コンパイラが不要な処理を無視できる可能性についてできるだけ詳しくまとめなさい。(この問題を作るのに Steve Haflich の助けを借りました。)
(defclass frob (standard-object) ())

(defmethod foo ((baz frob))
  (loop initially (mangle)
        while baz do
        (etypecase baz
          (frob (setf baz (bar baz)))))))