#:g1: 定番メソッドコンビネーション紹介: standard

Posted 2018-12-01 20:40:27 GMT

Lisp メソッドコンビネーション Advent Calendar 2018 2日目 》

ほぼ無計画なので、内容が続き物だったりそうではなかったりします。
とりあえず、定番というか、Common Lispに標準で用意されているメソッドコンビネーションの紹介でもしていこうかなと思います。

standardメソッドコンビネーション

Common Lispのdefmethodで定義したものは、標準でこのタイプになります。
主な登場人物は、:before:after:aroundcall-next-methodです。
:before:after:aroundは、指定したコンビネーションでのメソッドの追加で利用し、call-next-methodは、優先順位リストに従って次のメソッドを起動します。
他のOOP言語では、superに相当しますが、Common Lispでは、必ずしも継承順位で上位ものを呼び出すわけではないので、call-nextなのでしょう。

とりあえず、実際に:before:after:aroundcall-next-method全部盛りのメソッドを定義して動作を確認してみましょう。

(progn
  (defclass c1 () ())
  (defclass c2 (c1) ())
  (defclass c3 (c2) ()))

(progn (defmethod foo ((o c1)) (format T "~16T~A~%" '(foo c1))) (defmethod foo ((o c2)) (format T "~16T~A~%" '(foo c2)) (call-next-method)) (defmethod foo ((o c3)) (format T "~16T~A~%" '(foo c3)) (call-next-method)) ;; (defmethod foo :before ((o c1)) (format T "~8T~A~%" '(foo c1 :before))) (defmethod foo :before ((o c2)) (format T "~8T~A~%" '(foo c2 :before))) (defmethod foo :before ((o c3)) (format T "~8T~A~%" '(foo c3 :before))) ;; (defmethod foo :after ((o c1)) (format T "~24T~A~%" '(foo c1 :after))) (defmethod foo :after ((o c2)) (format T "~24T~A~%" '(foo c2 :after))) (defmethod foo :after ((o c3)) (format T "~24T~A~%" '(foo c3 :after))) ;; (defmethod foo :around ((o c1)) (format T "~A~%" '(foo c1 :around)) (call-next-method)) (defmethod foo :around ((o c2)) (format T "~A~%" '(foo c2 :around)) (call-next-method)) (defmethod foo :around ((o c3)) (format T "~A~%" '(foo c3 :around)) (call-next-method)))

とりあえず、実行してみるとこんな感じになります。

(foo (make-instance 'c3))(foo c3 around)(foo c2 around)(foo c1 around)(foo c3 before)(foo c2 before)(foo c1 before)(foo c3)(foo c2)(foo c1)(foo c1 after)(foo c2 after)(foo c3 after)
→ nil

大元のメソッドは、プライマリメソッドと呼びますが、call-next-methodで次のメソッドを呼ぶことが可能です。
上記では、(foo c3)(foo c2)で明示的に呼び出していますが、もちろん無ければ呼ばれません。

:beforeは、プライマリメソッド起動の前に起動されますが、クラス優先順位リストの順に該当するものは全て呼び出されます。
:afterは、:beforeと対称の動作です。 なお、:before:afterの中では、call-next-methodは使用できません。

:aroundが割合に複雑ですが、:aroundがあれば、それが最初に起動されます。
その:aroundの中で、call-next-methodが起動されれば、クラス優先順位リスト順に、次の:aroundを起動、:aroundがなければプライマリメソッドを起動します。
call-next-methodを呼べば、その返り値が利用できるので、このデフォルト値を加工するような使い方が殆どです。

:before:afterではcall-next-methodが使えないので、スロットの値を設定する等、副作用目的での利用となります。 GoFのObserverパターンのようなものは、呼び出しイベント時に起動したいフックのようなものが多いので、:before:afterで賄うことが可能かなと思います。

メソッドの中身を覗いてみる

compute-effective-methodで確認すると、call-methodの連鎖が直接見えて判り易いので確認してみると下記のようになります。

call-methodは第一引数に起動するメソッド、第二引数にそれ以降で起動するメソッドのリストを取りますが、入れ子にしていけば、所謂、継続渡しに似た記述になります。
起動リストが空か、メソッドのボディにcall-next-methodの記述がなければ、以降のメソッドは起動されず、そこで処理はストップします。

(c2mop:compute-effective-method #'foo
                                (c2mop:find-method-combination #'foo 'standard nil)
                                (compute-applicable-methods #'foo (list (make-instance 'c3))))

(call-method #<standard-method foo (:around) (c3) 4190211F13> (#<standard-method foo (:around) (c2) 4190211F2B> #<standard-method foo (:around) (c1) 4190211C93> (make-method (progn (call-method #<standard-method foo (:before) (c3) 419021266B> '()) (call-method #<standard-method foo (:before) (c2) 4190212683> '()) (call-method #<standard-method foo (:before) (c1) 419021258B> '()) (multiple-value-prog1 (call-method #<standard-method foo nil (c3) 4190212753> (#<standard-method foo nil (c2) 419021276B> #<standard-method foo nil (c1) 419021269B>)) (call-method #<standard-method foo (:after) (c1) 4190211F43> '()) (call-method #<standard-method foo (:after) (c2) 4190212573> '()) (call-method #<standard-method foo (:after) (c3) 419021255B> '()))))))

元祖Flavorsのデフォルトコンビネーション

元祖Flavorsでは、当初デフォルトのメソッドコンビネーションとして、:daemonコンビネーションが用意されていましたが、登場したのは大体1981年頃のようです。
これは、:before、プライマリ、:afterの組み合わせで、Common Lispのstandardからcall-next-method:aroundを削ったような挙動ですが、1984年あたりになると、:aroundも追加された様子。
call-next-methodのようなものはなく、#'(:method flavor method-name)のような形式で直接呼び出したり、:around専用の継続メソッドを呼び出す構文を使ったようです。

次回は、andコンビネーションあたりを紹介しようと思います。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus