#:g1: カジュアルにメソッドコンビネーション定義してみよう

Posted 2018-12-05 07:11:18 GMT

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

組み込みのメソッドコンビネーションのパターンを淡々と紹介していこうかなと思いましたが、eshamsterさんの記事が面白かったので、自分も簡単なものを考えて定義してみることにしました。

フィルターでメソッドコンビネーションが使えないか

たまたま今日GUIの一覧メニューのフィルターを作成していて、フィルターってメソッドコンビネーションで上手く書けるのでは?と思ったので試してみます。
とりあえず、リスト対して絞り込みの関数を順にandな感じで適用していく、という感じです。
通常なら、下記のようにでも書くのかなという所です。

(flet ((filter-integerp (list)
         (remove-if-not #'integerp list))
       (filter-evenp (list)
         (remove-if-not #'evenp list))
       (filter-flatten (list)
         (flatten list)))
  (defparameter *fns* (list #'filter-flatten
                            #'filter-integerp
                            #'filter-evenp)))

(reduce (lambda (res f) (funcall f res)) *fns* :initial-value '((1 2 3 4 (1 2 3 4 nil t 8) t 8) (1 2 3 4 (1 2 3 4 nil t 8) t 8)))(2 4 2 4 8 8 2 4 2 4 8 8)

フィルターをメソッドコンビネーションで書いてみよう

下準備

メソッドコンビネーションを定義するにあたって、コンビネーションがどう展開されるかを確認しつつ書きたいので展開ユーティリティを定義すると良いかなと思います。

(ql:quickload :closer-mop)

(defun mc-expand (gf mc mcopts &rest args) (c2mop:compute-effective-method gf (c2mop:find-method-combination gf mc mcopts) (compute-applicable-methods gf args)))

これでこんな感じに展開できます。

(mc-expand #'foo 
           'chain
           '()
           '(1 2 3 4 nil t 8))(progn
  (call-method
   #<standard-method foo (chain evenp-filter) (list) 4020328873>
   ((make-method
     (call-method
      #<standard-method foo (chain integerp-filter) (list) 4020231D53>
      nil)))))  

展開を確認しながら書けるのであれば、謎のdefine-method-combinationも使いこなせる気がしてきます。

書いてみる

さて、とりあえずは、フィルター適用の順序不定で良しとして、こんな感じに書いてみました。
標準のメソッドコンビネーションでは、:before:after等、修飾子の指定は一つですが、実際はこの部分はリストになっていて、equalで等価判定されます。
定義されるメソッドが違う名前であれば、クラス関係に関係なく増やすことが可能です。

(define-method-combination chain ()
  ((methods (chain . *)))
  `(progn 
     ,(reduce (lambda (m ms)
                `(call-method ,m ,(and ms `((make-method ,ms)))))
              methods
              :initial-value nil
              :from-end T)))

(defgeneric foo (x) (:method-combination chain))

(defmethod foo chain integerp-filter ((u list)) (remove-if-not #'integerp (call-next-method)))

(defmethod foo chain evenp-filter ((u list)) (remove-if-not #'evenp (call-next-method)))

(defmethod no-next-method ((gf (eql #'foo)) method &rest args) (apply #'identity args))

(foo '(1 2 3 4 nil t 8))
→ (2 4 8) 

call-next-methodで次を呼ばないといけないのがめんどうな気がしますが、処理結果を次に渡す安直で良い方法が分かりません(メソッドを分解して関数を取り出しても良いのですが……)
順不同で登録/呼びだしをするのでno-next-methodでデフォルト値を返すようにしています。

やっぱり順番を指定したい

やっぱり適用順によってはエラーになるようなフィルター構成もあるので、順番は指定しても良いかなと思ったので、優先順位を指定してみることにしました。

(defun ordered-chain-qualifier-p (method-qualifiers)
  (typep method-qualifiers
         '(cons (eql ordered-chain) (cons integer (cons T null)))))

(define-method-combination ordered-chain () ((methods ordered-chain-qualifier-p)) (let ((methods (remove-duplicates methods :key (lambda (method) (third (method-qualifiers method))) :from-end T))) `(progn ,(reduce (lambda (m ms) `(call-method ,m ,(and ms `((make-method ,ms))))) (stable-sort methods #'> :key (lambda (method) (second (method-qualifiers method)))) :initial-value nil :from-end T))))

(defgeneric bar (x) (:method-combination ordered-chain)))

(defmethod bar ordered-chain 0 identity ((u list)) u)

(defmethod bar ordered-chain 100 integerp-filter ((u list)) (remove-if-not #'integerp (call-next-method)))

(defmethod bar ordered-chain 200 evenp-filter ((u list)) (remove-if-not #'evenp (call-next-method)))

(defmethod bar ordered-chain 1 flatten-filter ((u list)) (flatten (call-next-method)))

展開はこんな感じです。

(mc-expand #'bar
           'ordered-chain
           '()
           '(1 2 3 4 nil t 8))(progn
  (call-method
   #<standard-method bar (ordered-chain 200 evenp-filter) (list) 4020447F83>
   ((make-method
     (call-method
      #<standard-method bar (ordered-chain
                             100
                             integerp-filter) (list) 402031D65B>
      ((make-method
        (call-method
         #<standard-method bar (ordered-chain
                                1
                                flatten-filter) (list) 4020458C5B>
         ((make-method
           (call-method
            #<standard-method bar (ordered-chain 0 identity) (list) 4020467533>
            nil)))))))))))

メソッドコンビネーション修飾子の二番目に整数で優先度を付けてみました。
パターンマッチでも行けると思いますが、ordered-chain-qualifier-pを定義して選別しています。

メソッドコンビネーション修飾子はequalで判定されると上述しましたが、優先度を変更して再定義するとメソッドがどんどん増えてしまうので、重複も削除しています。
また、デフォルトメソッドを最優先として定義すればno-next-methodの定義も不要になります。

さて実行してみると、

(bar '((1 2 3 4 (1 2 3 4 nil t 8) t 8) (1 2 3 4 (1 2 3 4 nil t 8) t 8)))(2 4 2 4 8 8 2 4 2 4 8 8)  

とりあえずは、3つのフィルターが順に適用されているようです。

問題点

しかし、優先度を付けてみると、今度は、call-next-methodと優先番号の組み合わせが直感的でない気がします。

具体的には、優先番号が低い順から呼ばれるのに、引数の適用は優先度の高い方からされる、ということで、call-next-methodで呼ばない場合、連鎖は優先度が低い方から途切れてしまいます。

call-methodの入れ子にしないで、平坦に優先順に並べ、適用結果を次々に受け渡すことができれば、整合性は取れる筈ですが……。

メソッドコンビネーションアドベントカレンダー終了まで良い解決策が思い付けば記事のネタにしたいと思います。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus