Posted 2018-12-01 20:40:27 GMT
《 Lisp メソッドコンビネーション Advent Calendar 2018 2日目 》
ほぼ無計画なので、内容が続き物だったりそうではなかったりします。
とりあえず、定番というか、Common Lispに標準で用意されているメソッドコンビネーションの紹介でもしていこうかなと思います。
standard
メソッドコンビネーションCommon Lispのdefmethod
で定義したものは、標準でこのタイプになります。
主な登場人物は、:before
、:after
、:around
、call-next-method
です。
:before
、:after
、:around
は、指定したコンビネーションでのメソッドの追加で利用し、call-next-method
は、優先順位リストに従って次のメソッドを起動します。
他のOOP言語では、super
に相当しますが、Common Lispでは、必ずしも継承順位で上位ものを呼び出すわけではないので、call-next
なのでしょう。
とりあえず、実際に:before
、:after
、:around
、call-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では、当初デフォルトのメソッドコンビネーションとして、: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