“实战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
修饰的方法之中。