The “Real Elisp” series is about my experience with customizing Emacs with Elisp, and I invite Emacs fans to share my experience with me — if there are any Emacs users out there, hahaha.

Emacs’ org-mode uses a markup language called org, which, like most markup languages, also supports unordered lists — the former prefixed by – (a hyphen, a space), the latter prefixed by – [] or – [x] (with an extra pair of square brackets and the middle letter x) and checklists.

In addition, org-mode provides the m-ret shortcut for editing both lists to quickly insert a new row (that is, hold down the Alt key and press Enter). If the cursor is in an unordered list, a new line is automatically inserted with the – prefix. Unfortunately, if the cursor is in the checklist, the new line does not automatically insert a pair of square brackets

It’s tedious to have to type in [] manually every time. The good news is that this is Emacs, which is extensible and customizable. With a few lines of code, you can ask Emacs to enter square brackets for you.

AOP features of Emacsadvice-add

Using Emacs’ describe-key functionality, you know that when you press m-ret in an org-mode file, Emacs calls the org-insert-item function. For M-RET to automatically append square brackets, you can immediately think of a simple and crude solution:

  • Define a new function and set it toM-RETBind to it;
  • To redefine theorg-insert-itemFunction to append square brackets;

But either way, you need to reimplement the existing function of inserting hyphens and space prefixes. A more modest way to extend the behavior of the existing org-insert-item is the advice feature of Emacs.

Advice is one of the facet-oriented programming paradigms, and using Emacs’ advice-add function, you can do something incidentally before or after a normal function is called — such as appending a pair of square brackets. For these two opportunities, you can directly use advice-add :before and :after, respectively, but neither is appropriate here because:

  • Check if it is in the checklist and needs to be calledorg-insert-itemBefore do;
  • If you append a pair of square brackets, you need to set theorg-insert-itemAfter done.

Therefore, the correct thing to do is to decorate the original org-insert-item function with :around

(cl-defun lt-around-org-insert-item (oldFunction &rest args) "To append [] after the org-insert-item is called." (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)); Continue to insert text using the original org-insert-item (apply oldFunction args); When is-checkbox (insert "[] ")))) (advice-add 'org-insert-item :around #'lt-around-org-insert-item)

The M-RET now treats checklists equally

The Common Lispmethod combination

The :after, :around, and :before of advice-add have identical equivalents in Common Lisp, except that instead of using a function called advice-add, they are fed to a macro called defmethod. For example, defmethod can be used to define a polymorphic len function that performs different logic for different types of input arguments

(defgeneric len (x))

(defmethod len ((x string))
  (length x))

(defmethod len ((x hash-table))
  (hash-table-count x))

We then define the corresponding :after, :around, and :before methods for specialized versions where the parameter type is a string

(defmethod len :after ((x string)) (format t "after len~%")) (defmethod len :around ((x string)) (format t Prog1 (call-next-method) (format t "around after calling len ~%"))) (defmethod len :before ((x string)) (format t "before len~%"))

The call rule for this series of methods is:

  1. First call:aroundA method of decoration;
  2. Because the above method is calledcall-next-method, so call again:beforeA method of decoration;
  3. Call an undecorated method (in CL this is calledprimaryMethods);
  4. Call again:afterA method of decoration;
  5. And then finally, it comes back:aroundIn the callcall-next-methodThe location of the.

At first glance, Emacs advice-add supports many more modifiers than it actually does. In CL, :after, :around, and :before are all part of a method combination called standard, and there are other method combinations built into CL. In the Other Method Triangle section, the author demonstrates examples of the Progn and List combinations.

If you want to emulate the other modifiers supported by Emacs advice-add, you must define a new method combination.

Programmable programming language —define-method-combination

I used to think that defmethod could only accept :after, :around, and :before, considering these three modifiers to be features that must be supported at the language level. It wasn’t until one day that I broke into Lispworks’s Define-Method-Combination entry that I realized they were just three trivial modifiers.

(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))))

In keeping with the principle of “pick the soft ones”, let me try to simulate the effects of advice-add :after-while and :before-while.

The :after-while and :before-while effects are easy to understand

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.

Therefore, in the form generated by Define-Method-Combination (remember that I translated it as a form in PCL), there must be:

  • Check if there is any:before-whileA method of decoration;
  • If so, check that the call was made:before-whileWhether the return value after the modified method isNIL;
  • If not, or be:before-whileThe return value of the decorated method is notNIL, we will know how to callprimaryMethods;
  • If you have been:after-whileModify the method, andprimaryThe return value of the method is notNIL, these methods are called;
  • returnprimaryThe return value of the.

For simplicity, although after-while and before-while variables refer to multiple “callable” methods, only the “most concrete” one is called here.

This new method combination is called Emacs-advice, and its implementation is already a natural one

(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 (and its make-method partner) is a macro that is specifically used to call the incoming method within a define-method-combination.

Use a series of Foobar methods to verify this

(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) ;;  Return NIL (foobar 2); Print "FO Side Effect" and return Hello

Afterword.

Despite my love for CL, the more I thought about Define-Method-Combination, the more I realized that there are limits to what a programming language can do, unless it’s beyond the language. For example, Emacs advice-add supports :filter-args and :filter-return that can’t be elegantly implemented with define-method-combination — not at all. You just need to combine them in the method modified with :around.

Read the original