Hatena::ブログ(Diary)

Common Lisp クックブック

Common 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 の無限エクステントの効果を試してみなさい。