從AOP到method-combination

「實戰Elisp」系列旨在講述我使用Elisp定製Emacs的經驗,拋磚引玉,還請廣大Emacs同好不吝賜教——若是真的有廣大Emacs用戶的話,哈哈哈。

Emacs的org-mode用的是一門叫Org的標記語言,正如大部分的標記語言那樣,它也支持無序列表和檢查清單——前者以- (一個連字符、一個空格)爲前綴,後者以- [ ] - [x] 爲前綴(比無序列表多了一對方括號及中間的字母xhtml

此外,org-mode還爲編輯這兩種列表提供了快速插入新一行的快捷鍵M-RET(即按住alt鍵並按下回車鍵)。若是光標位於無序列表中,那麼新的一行將會自動插入- 前綴。遺憾的是,若是光標位於檢查清單中,那麼新一行並無自動插入一對方括號git

每次都要手動敲入 [ ] 還挺繁瑣的。好在這是Emacs,它是可擴展的、可定製的。只需敲幾行代碼,就可讓Emacs代勞輸入方括號了。github

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特性。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對檢查清單也一視同仁了函數

Common Lisp的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~%"))

這一系列方法的調用規則爲:

  1. 先調用:around修飾的方法;
  2. 因爲上述方法中調用了call-next-method,所以再調用:before修飾的方法;
  3. 調用不加修飾的方法(在CL中這稱爲primary方法);
  4. 再調用:after修飾的方法;
  5. 最後,又回到了:around中調用call-next-method的位置。

咋看之下,Emacs的advice-add支持的修飾符要多得多,實則否則。在CL中,:after:around,以及:before同屬於一個名爲standardmethod combination,而CL還內置了其它的method combination。在《Other method combinations》一節中,做者演示了prognlist的例子。

若是想要模擬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-whilebefore-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修飾的方法之中。

閱讀原文

相關文章
相關標籤/搜索