Hatena::ブログ(Diary)

Common Lisp クックブック

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

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)))))))