「實戰Elisp」系列旨在講述我使用Elisp定製Emacs的經驗,拋磚引玉,還請廣大Emacs同好不吝賜教——若是真的有廣大Emacs用戶的話,哈哈哈。
Emacs的org-mode用的是一門叫Org的標記語言,正如大部分的標記語言那樣,它也支持無序列表和檢查清單——前者以-
(一個連字符、一個空格)爲前綴,後者以- [ ]
或- [x]
爲前綴(比無序列表多了一對方括號及中間的字母x
)html
此外,org-mode還爲編輯這兩種列表提供了快速插入新一行的快捷鍵M-RET
(即按住alt
鍵並按下回車鍵)。若是光標位於無序列表中,那麼新的一行將會自動插入-
前綴。遺憾的是,若是光標位於檢查清單中,那麼新一行並無自動插入一對方括號git
每次都要手動敲入 [ ]
還挺繁瑣的。好在這是Emacs,它是可擴展的、可定製的。只需敲幾行代碼,就可讓Emacs代勞輸入方括號了。github
advice-add
藉助Emacs的describe-key
功能,能夠知道在一個org-mode
的文件中按下M-RET
時,Emacs會調用到函數org-insert-item
上。要想讓M-RET
實現自動追加方括號的效果,立刻能夠想到簡單粗暴的辦法:編程
M-RET
綁定到它身上;org-insert-item
函數,使其追加方括號;但無論是上述的哪種,都須要連帶着從新實現插入連字符、空格前綴的已有功能。有一種更溫和的辦法能夠在現有的org-insert-item
的基礎上擴展它的行爲,那就是Emacs的advice
特性。app
advice
是面向切面編程範式的一種,使用Emacs的advice-add
函數,能夠在一個普通的函數被調用前或被調用後捎帶作一些事情——好比追加一對方括號。對於這兩個時機,分別能夠直接用advice-add
的:before
和:after
來實現,但用在這裏都不合適,由於:編程語言
org-insert-item
前作;org-insert-item
以後作。所以,正確的作法是使用:around
來修飾原始的org-insert-item
函數ide
(cl-defun lt-around-org-insert-item (oldfunction &rest args) "在調用了org-insert-item後識時務地追加 [ ]這樣的內容。" (let ((is-checkbox nil) (line (buffer-substring-no-properties (line-beginning-position) (line-end-position)))) ;; 檢查當前行是否爲checkbox (when (string-match-p "- \\[.\\]" line) (setf is-checkbox t)) ;; 繼續使用原來的org-insert-item插入文本 (apply oldfunction args) ;; 決定要不要追加「 [ ]」字符串 (when is-checkbox (insert "[ ] ")))) (advice-add 'org-insert-item :around #'lt-around-org-insert-item)
這下子,M-RET
對檢查清單也一視同仁了函數
method combination
advice-add
的:after
、:around
,以及:before
在Common Lisp中有着徹底同名的等價物,只不過不是用一個叫advice-add
的函數,而是餵給一個叫defmethod
的宏。舉個例子,用defmethod
能夠定義出一個多態的len
函數,對不一樣類型的入參執行不一樣的邏輯ui
(defgeneric len (x)) (defmethod len ((x string)) (length x)) (defmethod len ((x hash-table)) (hash-table-count x))
而後爲其中參數類型爲字符串的特化版本定義對應的:after
、:around
,以及:before
修飾過的方法spa
(defmethod len :after ((x string)) (format t "after len~%")) (defmethod len :around ((x string)) (format t "around中調用len前~%") (prog1 (call-next-method) (format t "around中調用len後~%"))) (defmethod len :before ((x string)) (format t "before len~%"))
這一系列方法的調用規則爲:
:around
修飾的方法;call-next-method
,所以再調用:before
修飾的方法;primary
方法);:after
修飾的方法;:around
中調用call-next-method
的位置。咋看之下,Emacs的advice-add
支持的修飾符要多得多,實則否則。在CL中,:after
、:around
,以及:before
同屬於一個名爲standard
的method combination
,而CL還內置了其它的method combination
。在《Other method combinations》一節中,做者演示了progn
和list
的例子。
若是想要模擬Emacs的advice-add
所支持的其它修飾符,那麼就必須定義新的method combination
了。
define-method-combination
曾經我覺得,defmethod
只能接受:after
、:around
,以及:before
,認爲這三個修飾符是必須在語言一級支持的特性。直到有一天我闖入了LispWorks的define-method-combination詞條中,才發現它們也是三個平凡的修飾符而已。
(define-method-combination standard () ((around (:around)) (before (:before)) (primary () :required t) (after (:after))) (flet ((call-methods (methods) (mapcar #'(lambda (method) `(call-method ,method)) methods))) (let ((form (if (or before after (rest primary)) `(multiple-value-prog1 (progn ,@(call-methods before) (call-method ,(first primary) ,(rest primary))) ,@(call-methods (reverse after))) `(call-method ,(first primary))))) (if around `(call-method ,(first around) (,@(rest around) (make-method ,form))) form))))
秉持「柿子要挑軟的捏」的原則,讓我來嘗試模擬出advice-add
的:after-while
和:before-while
的效果吧。
:after-while
和:before-while
的效果仍是很容易理解的
Call function after the old function and only if the old function returned non-
nil
.Call function before the old function and don’t call the old function if function returns
nil
.
所以,由define-method-combination
生成的form
中(猶記得傘哥在《PCL》中將它翻譯爲形式),勢必要:
:before-while
修飾的方法;:before-while
修飾的方法後的返回值是否爲NIL
;:before-while
修飾的方法的返回值爲非NIL
,便調用primary
方法;:after-while
修飾的方法,而且primary
方法的返回值不爲NIL
,就調用這些方法;primary
方法的返回值。爲了簡單起見,儘管after-while
和before-while
變量指向的是多個「可調用」的方法,但這裏只調用「最具體」的一個。
給這個新的method combination
取名爲emacs-advice
,其具體實現已經是水到渠成
(define-method-combination emacs-advice () ((after-while (:after-while)) (before-while (:before-while)) (primary () :required t)) (let ((after-while-fn (first after-while)) (before-while-fn (first before-while)) (result (gensym))) `(let ((,result (when ,before-while-fn (call-method ,before-while-fn)))) (when (or (null ,before-while-fn) ,result) (let ((,result (call-method ,(first primary)))) (when (and ,result ,after-while-fn) (call-method ,after-while-fn)) ,result)))))
call-method
(以及它的搭檔make-method
)是專門用於在define-method-combination
中調用傳入的方法的宏。
用一系列foobar
方法來驗證一番
(defgeneric foobar (x) (:method-combination emacs-advice)) (defmethod foobar (x) 'hello) (defmethod foobar :after-while (x) (declare (ignorable x)) (format t "for side effect~%")) (defmethod foobar :before-while (x) (evenp x)) (foobar 1) ;; 返回NIL (foobar 2) ;; 打印「fo side effect」,並返回HELLO
儘管我對CL賞識有加,但越是琢磨define-method-combination
,就越會發現編程語言的能力是有極限的,除非超越編程語言。好比Emacs的advice-add
所支持的:filter-args
和:filter-return
就沒法用define-method-combination
優雅地實現出來——並非徹底不行,只不過須要將它們合併在由:around
修飾的方法之中。