#:g1: MOPでディスパッチの多様化: filtered-functions

Posted 2013-12-17 16:05:00 GMT

(Metaobject Protocol(MOP) Advent Calendar 2013参加エントリ)

 Metaobject Protocol(MOP) Advent Calendar 2013 18日目です。

filtered-functionsを眺める

 今回は、ネタも無いということで、MOPの応用例を眺めてみよう、ということで、filtered-functionsを眺めてみます。

filtered-functionsとは

 総称関数のディスパッチにフィルタリングする関数を付けてみよう、というアイデアです。
詳しくは、配布元のページと、このページにある論文を参照して下さい。

とりあえず利用例

 シンプルな利用例としては、こんな感じです

まず、総称関数のベースを定義
(define-filtered-function fib (n)
  (:filters (:signum #'signum)))
フィルターは複数取れますが、ここでは一つ指定しています。
(:フィルター名 #'関数)
という構成になっていて、フィルター名をdefmethodのメソッドコンビネーションの引数で指定するようになっています。
次にメソッド
(defmethod fib ((n integer))
  (if (< n 2)
      n
      (+ (fib (1- n))
         (fib (- n 2)))))
こちらは普通にintegerに対して定義
(defmethod fib :filter :signum 
           ((n (eql -1)))
  (error "fib not defined for negative numbers: ~A." n))
そして、これが、
(eql (signum x) -1)
の場合に起動されるメソッドです。
こんな感じで、メソッドに引数が渡される前に、フィルター関数で処理される、という訳です。
(fib 10)
;=>  55

(fib -8)
;>!! fib not defined for negative numbers: -8.

filtered-functionsの中身を読んでみる

さて、どういう仕組みで実現されているのか眺めてみますが、肝となるのは、compute-applicable-methods と、 compute-applicable-methods-using-classes と、compute-discriminating-function です。
まず、フィルター関数がどこで実行されているのかというと、 compute-applicable-methods で
compute-applicable-methods
(defmethod compute-applicable-methods ((ff simple-filtered-function) required-args)
  (let* ((filter-expression (generic-function-filter-expression ff))
         (filter-functions (apply filter-expression required-args)))
    (cond ((consp filter-functions)
            (loop for arg in required-args
                  for filter-function = (pop filter-functions)
                  collect (if filter-function
                              (funcall filter-function arg)
                              arg)
                    into filtered-args
                  finally (return (call-next-method ff filtered-args))))
           ((null filter-functions) '())
           ((eq filter-functions 't) (call-next-method))
           (t (call-next-method ff (cons (funcall filter-functions (first required-args))
                                         (rest required-args)))))))
の (funcall filter-function arg) になります。
そしてfilter-functionで処理済みの引数が上位メソッドに渡されます。つまり上の例でいうとfibに渡された-8が、signumで-1にされるところです。あとは、-1が上位に渡されます。そしてそれから、(eql -1)の条件に掴まるという訳です。
compute-applicable-methods-using-classes
次に謎の定義の compute-applicable-methods-using-classes ですが、
(defmethod compute-applicable-methods-using-classes ((ff simple-filtered-function) classes)
  (declare (ignore classes))
  (values '() nil))

となっています。注目するところは、多値の第二値でnilを返しているところです。

 何故にnilを返しているかというと、compute-discriminating-function では、compute-applicable-methods-using-classes の返り値の第二値を見て、キャッシュした値を使うか、再計算するかを決定するからで、nilの場合は、 compute-applicable-methods が呼ばれるので、毎度nilが返ってくるということは、キャッシュされず、毎度 compute-applicable-methods が呼ばれるということになり、フィルター関数も毎度実行される、という訳です。
もちろん、simple-filtered-function の場合だけ毎度呼ばれるのみで、上位メソッドでは通常通りになります。

その他

 あとは、メソッドコンビネーションの定義が、 define-method-combination 等で色々とありますので、参考になるでしょう。
さらに、単発のフィルターを持つ simple-filtered-function クラスと、複数のフィルターを持つクラスの、filter-functionクラスに分かれているので、複数の場合の処理も眺めてみると良いと思います。

まとめ

 filter-functionsは、既存の枠組みを活かした面白い拡張だと思います。コードも250行位と短いので、総称関数を拡張する場合には参考になるのではないかと思いますので是非読んでみて下さい。

 MOP Advent Calendarも残すところあと一週間。MOPネタならどんなネタでもOKですので参加お待ちしています!

comments powered by Disqus