Emacs Lispの関数で再定義なしに挙動を変更するには
古くから defadvice による アドバイス が使われていました。
Emacs 24.4では古くから使われている advice.el に代わって
nadvice.el がアドバイス定義をするようになりました。
旧来のアドバイスはあまりにも複雑すぎました。
しかも ad-do-it だの ad-return-value などという
変数というかシンボルマクロというか…謎の代物を使うマクロでした。
新しいアドバイスは関数で閉じているのでシンプルになりました。
行数を見ても明らかです。
- advice.el 3276行
- nadvice.el 509行
行数実に1/6!
それでは、新旧アドバイスの挙動を比較してみましょう。
アドバイスでの挙動を観察するには、
実行経路と関数の戻り値を調べます。
defadvice:startは、defadvice:1の返り値と実行経路を返します。
defadvice:resetは、アドバイスをすべて解除します。
旧来の方法 141030131228.defadvice.el(以下のコードと同一)
それでは旧来のdefadviceの挙動を確認しましょう。
(defun defadvice:start () (let (lst) `(:return ,(defadvice:1 256) :route ,(nreverse lst)))) (defun defadvice:reset () "`defadvice:1'にかけられたadviceを全部削除する" (ad-disable-regexp "defadvice-test-") (ad-update 'defadvice:1)) ;;; 元の挙動 (defun defadvice:1 (x) (push `(original ,x) lst) 'original) (defadvice:reset) (defadvice:start) ;; => (:return original :route ;; ((original 256))) ;;; beforeアドバイス (defadvice defadvice:1 (before defadvice-test-before activate) (push `(before ,x) lst) 'before) (defadvice:start) ;; => (:return original :route ;; ((before 256) ;; (original 256))) ;;; afterアドバイス (defadvice defadvice:1 (after defadvice-test-after activate) (push `(after ,x) lst) 'after) (defadvice:start) ;; => (:return original :route ;; ((before 256) ;; (original 256) ;; (after 256))) ;;; aroundアドバイス (defadvice defadvice:1 (around defadvice-test-around activate) (push `(around-1 ,x) lst) ad-do-it (push `(around-2 ,x) lst) (setq ad-return-value 'around)) (defadvice:start) ;; => (:return around :route ;; ((before 256) ;; (around-1 256) ;; (original 256) ;; (around-2 256) ;; (after 256)))
新しい方法 141030111602.advice-add.el(以下のコードと同一)
新しい方法では、 advice-add と advice-remove を使います。
(advice-add '関数名 場所 'アドバイス関数名)
新しい方法でアドバイスを定義するには、アドバイス関数を定義し、
advice-addを呼び出します。
場所は主にこの3つです。
- :before
- :after
- :around
(advice-remove '関数名 'アドバイス関数名)
アドバイスを解除するには、advice-removeでアドバイス関数を指定するだけです。
場所指定も ad-update も不要になったのは嬉しいですね。
aroundアドバイスの定義は見違えるようにすっきりしました。
ad-do-itの代わりに元の関数が変数になったことで、
apply を使えばいいことになります。
また、ad-return-valueを設定せずとも
そのままアドバイス関数の返り値が
関数の返り値になりました。
(defun advice:start ()
(let (lst)
`(:return ,(advice:1 256)
:route ,(nreverse lst))))
(defun advice:reset ()
(dolist (where '(before after around override filter-return
filter-args true false))
(advice-remove 'advice:1 (intern (format "advice:%s" where)))))
;;; 元の挙動
(defun advice:1 (x) (push `(original ,x) lst) 'original)
(advice:reset)
(advice:start)
;; => (:return original :route
;; ((original 256)))
;;; beforeアドバイス
(defun advice:before (x) (push `(before ,x) lst) 'before)
(advice-add 'advice:1 :before 'advice:before)
(advice:start)
;; => (:return original :route
;; ((before 256)
;; (original 256)))
;;; afterアドバイス
(defun advice:after (x) (push `(after ,x) lst) 'after)
(advice-add 'advice:1 :after 'advice:after)
(advice:start)
;; => (:return original :route
;; ((before 256)
;; (original 256)
;; (after 256)))
;;; aroundアドバイス
(defun advice:around (orig-func &rest args)
(push `(around-1 ,args) lst)
(apply orig-func args)
(push `(around-2 ,args) lst)
'around)
(advice-add 'advice:1 :around 'advice:around)
(advice:start)
;; => (:return around :route
;; ((around-1
;; (256))
;; (before 256)
;; (original 256)
;; (after 256)
;; (around-2
;; (256))))
結果を比較してわかるように、aroundの実行順序が変わりました。
defadviceでは内側にくっついていますが、
advice-addでは外側にくっついていることがわかります。
他のアドバイス 141030132213.advice-add.2.el(以下のコードと同一)
新しいアドバイスでは、それ以外のアドバイスも定義できます。
とはいっても、どれもaroundアドバイスで定義できるのですが、
新しい語彙が増えることで、表現力が向上しました。
- :override 純粋に関数再定義
- :filter-return 返り値を加工
- :filter-args 引数を加工
- :before-while アドバイス関数の返り値が真のときに本体を実行
- :before-until アドバイス関数の返り値がnilのときに本体を実行
- :after-while 本体の戻り値が真のときにアドバイス関数を実行?
- :after-until 本体の戻り値が偽のときにアドバイス関数を実行?
:filter-argsの返り値は新しい引数リストを返す必要があります。
;;; overrideアドバイス
(defun advice:override (x)
(push `(override ,x) lst)
'override)
(advice:reset)
(advice-add 'advice:1 :override 'advice:override)
(advice:start)
;; => (:return override :route
;; ((override 256)))
;;; filter-returnアドバイス
(defun advice:filter-return (x)
(intern (format "%s:filter-return" x)))
(advice:reset)
(advice-add 'advice:1 :filter-return 'advice:filter-return)
(advice:start)
;; => (:return original:filter-return :route
;; ((original 256)))
;;; filter-argsアドバイス
(defun advice:filter-args (args)
(list (* 2 (car args))))
(advice:reset)
(advice-add 'advice:1 :filter-args 'advice:filter-args)
(advice:start)
;; => (:return original :route
;; ((original 512)))
;;; before-while/before-until/after-while/after-untilアドバイス
(defun advice:true (x) (push `(true ,x) lst) t)
(defun advice:false (x) (push `(false ,x) lst) nil)
(advice:reset)
(advice-add 'advice:1 :before-while 'advice:true)
(advice:start)
;; => (:return original :route
;; ((true 256)
;; (original 256)))
(advice:reset)
(advice-add 'advice:1 :before-while 'advice:false)
(advice:start)
;; => (:return nil :route
;; ((false 256)))
(advice:reset)
(advice:reset)
(advice-add 'advice:1 :before-until 'advice:true)
(advice:start)
;; => (:return t :route
;; ((true 256)))
(advice:reset)
(advice-add 'advice:1 :before-until 'advice:false)
(advice:start)
;; => (:return original :route
;; ((false 256)
;; (original 256)))
(advice:reset)
(advice-add 'advice:1 :after-while 'advice:true)
(advice:start)
;; => (:return t :route
;; ((original 256)
;; (true 256)))
(advice:reset)
(advice-add 'advice:1 :after-while 'advice:false)
(advice:start)
;; => (:return nil :route
;; ((original 256)
;; (false 256)))
(advice:reset)
(advice-add 'advice:1 :after-until 'advice:true)
(advice:start)
;; => (:return original :route
;; ((original 256)))
(advice:reset)
(advice-add 'advice:1 :after-until 'advice:false)
(advice:start)
;; => (:return original :route
;; ((original 256)))
本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。