从AOP到method-combination
“实战Elisp”系列旨在讲述我使用Elisp定制Emacs的经验,抛砖引玉,还请广大Emacs同好不吝赐教——如果真的有广大Emacs用户的话,哈哈哈。Emacs的org-mode用的是一门叫Org的标记语言,正如大部分的标记语言那样,它也支持无序列表和检查清单——前者以
-
(一个连字符、一个空格)为前缀,后者以- [ ]
或- [x]
为前缀(比无序列表多了一对方括号及中间的字母x
)文章图片
【从AOP到method-combination】此外,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 returnsnil
.
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
,就越会发现编程语言的能力是有极限的advice-add
所支持的:filter-args
和:filter-return
就无法用define-method-combination
优雅地实现出来——并不是完全不行,只不过需要将它们合并在由:around
修饰的方法之中。阅读原文
推荐阅读
- 2018-02-06第三天|2018-02-06第三天 不能再了,反思到位就差改变
- 一个小故事,我的思考。
- Docker应用:容器间通信与Mariadb数据库主从复制
- 第三节|第三节 快乐和幸福(12)
- 你到家了吗
- 一个人的碎碎念
- 遇到一哭二闹三打滚的孩子,怎么办┃山伯教育
- 死结。
- 我从来不做坏事
- 赢在人生六项精进二阶Day3复盘