“实战Elisp”系列旨在讲述我使用Elisp定制Emacs的经验,抛砖引玉,还请广大Emacs同好不吝赐教——如果真的有广大Emacs用户的话,哈哈哈。
Emacs的org-mode用的是一门叫Org的标记语言,正如大部分的标记语言那样,它也支持无序列表和检查清单——前者以- (一个连字符、一个空格)为前缀,后者以- [ ] 或- [x] 为前缀(比无序列表多了一对方括号及中间的字母x)

此外,org-mode还为编辑这两种列表提供了快速插入新一行的快捷键M-RET(即按住alt键并按下回车键)。如果光标位于无序列表中,那么新的一行将会自动插入- 前缀。遗憾的是,如果光标位于检查清单中,那么新一行并没有自动插入一对方括号

每次都要手动敲入[ ] 还挺繁琐的。好在这是Emacs,它是可扩展的、可定制的。只需敲几行代码,就可以让Emacs代劳输入方括号了。
Emacs的AOP特性——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特性。
advice是面向切面编程范式的一种,使用Emacs的advice-add函数,可以在一个普通的函数被调用前或被调用后捎带做一些事情——比如追加一对方括号。对于这两个时机,分别可以直接用advice-add的:before和:after来实现,但用在这里都不合适,因为:
- 检测是否位于检查清单中,需要在调用org-insert-item前做;
- 追加一对方括号,则需要在org-insert-item之后做。
因此,正确的做法是使用:around来修饰原始的org-insert-item函数
(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对检查清单也一视同仁了

Common Lisp的method combination
advice-add的:after、:around,以及:before在Common Lisp中有着完全同名的等价物,只不过不是用一个叫advice-add的函数,而是喂给一个叫defmethod的宏。举个例子,用defmethod可以定义出一个多态的len函数,对不同类型的入参执行不同的逻辑
(defgeneric len (x))
(defmethod len ((x string))
  (length x))
(defmethod len ((x hash-table))
  (hash-table-count x))
复制代码然后为其中参数类型为字符串的特化版本定义对应的:after、:around,以及:before修饰过的方法
(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修饰的方法;
- 调用不加修饰的方法(在CL中这称为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修饰的方法之中。























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
