基于 pcase 的函数热补丁方法

基于我先前的一个包 psearch 实现。

适用于不便 advice 又不想改源码的场景,例如:一个巨长的函数,你只想改中间一两句。

改源代码当然是最稳妥的,但是改了至少得建个仓库来维护,不如 advice 和 patch 方便。

Melpa 上有一个看起来比较完善并且有不少人在用的包 el-patch,但是它很复杂,引入了很多概念,看得我头疼,而且写起来很累赘,几乎是把原函数抄一遍了。

例如把以下左边函数改成右边的形式:

(defun test-patch ()        ;; =>    (defun test-patch ()
  (list '(1 2 3)            ;;         (list '(1 2 3)
        (if nil '(4 5 6))   ;;               (if t '(4 5 6))
        '(7 8 9)))          ;;               '(7 8 9)))
  • psearch 的写法:

    (psearch-with-function-patch test-patch
      (psearch-replace '`(if nil ,body)
                       '`(if t ,body)))
    
  • el-patch 的写法:

    (el-patch-defun test-patch ()
      (list '(1 2 3)                            ;; 原样抄写
            (if (el-patch-swap nil t) '(4 5 6)) ;; 修改的部分
            '(7 8 9)))                          ;; 原样抄写
    

如果这个虚构的简单的例子让你感觉似乎 el-patch 更简单,那么你应该看看真实世界的例子:

在这个例子里,它 patch 的目标是 lisp-indent-function 函数,几乎是把原函数里面的语句全部「摘」出来,然后重新组合。

比如想改一个函数里的 (if cond then else) 结构,先用 el-patch-let 把各个部分代码「摘」出来,然后用 el-patch-swap 重新组合。「摘」的手法很原始粗暴:

(el-patch-defun a-function (some-args)
  ...
  .... 虽然没改到这部分代码,请一字不落抄下来 ...
  ...

  (el-patch-let (($cond (... 很长很长,原函数怎么写你就怎么写,一字不落 ...))
                 ($then (... 很长很长,原函数怎么写你就怎么写,一字不落 ...))
                 ($else (... 很长很长,原函数怎么写你就怎么写,一字不落 ...)))
    (el-patch-swap
      ;; 原先的形式
      (if $cond
          $then
        $else)
      ;; 新的形式
      (if $cond
          $else
       $then)))

  ...
  .... 虽然没改到这部分代码,也要一字不落抄下来 ...
  ...)

psearch 则使用 pcase 语法来定位代码,只要把关键部分代码特征交代清楚,不用面面俱到,也不用关心没改到的代码

(psearch-with-function-patch some-function (some-args)
  (psearch-replace
   ;; match ---------
   '`(if ,(and cond (guard (equal cond
                                  ;; 假设 cond 细节如下:
                                  '(or (string= char "\n")
                                       (string= char "\t")))))
         ,then
       ,else)
   ;; replace -------
   '`(if ,cond
         ,else
       ,then)))

如果某个 form 在函数里出现多次而且内容大同小异,你又不想把匹配规则写太细,还可以用位置来匹配,例如:

(with-eval-after-load 'corfu-doc-terminal
  (psearch-with-function-patch corfu-doc-terminal--preprocess-docstring
    ;; 删除所有 `(let ...)' 但保留第一个
    (let ((pattern '`(let . ,rest)))
      (and (psearch-forward pattern)
           (psearch-replace pattern nil)))))

确保每个 pattern 都写对了,并且执行成功。


:warning: 已知问题:patch 很容易丢失,比如 edbug 或 eval 过原函数,就必需重新 patch。el-patch 也存在这个问题。

所以代码暂时放到 function-patch 分支,通过以下方法安装体验:

(quelpa '(psearch
          :fetcher github
          :repo "twlz0ne/psearch.el"
          :branch "function-patch"
          :files ("psearch.el")))

:bulb: 我在考虑是不是改为生成一个打过补丁的 advice 函数,然后用这个函数覆盖原函数,这样就不容易丢失了,然后 describe-function 也能看到这个函数被覆盖了。


相关主题:psearch: 基于 pcase 的 elisp 代码搜索工具

7 个赞

el-patch validate 的功能才是吸引人的地方,可以随时验证,被patch的源代码是不是发生了变化,这样上游的变化引起的问题,就不用担心了。 且可以比较patch和源码的差别。如果只是patch的话,el-patch的做法的确不够精简,不如直接再相关代码eval后再eval修改的patch代码。

有了 patch 和源码,validate 是可以做到的。

不管是动态的 patch,还是 .patch 文件形式的补丁,都允许源代码一定程度的改变,直到有一天 patch 失败(psearch 会抛出错误信息)。

所以 patch 本身就承担了部分 validate 工作。

不知道别人的习惯,我写好 advice 之后不会时时去看源码是否发生变化,通常是等到发现行为超出预期之后,才会去查看。如果确实不放心,不妨写几些测试用例。patch 同理。

一旦 patch 有问题,如果 patch 的写法很直观,很容易让人看懂它的意图,对照 patch 看源代码也很容易发现问题所在。el-patch validate 算是锦上添花。

曲高和寡。。。太高级了,不会玩。。。 :crazy_face:

pcase 是 Emacs 24 开始内置的一个包:

实乃居家旅行,消解累赘,令代码倍加清爽的利器。

用法可参考 @xuchunyang 的文章《 pcase 各种 pattern 使用举例 》。

1 个赞

最近试了试, 确实很好用, 不过如果能像 advice 那样, 既可以 add 也可以 remove 的话, 就会更好用了, 不知作者什么时候能实现

很早就实现过,但是被我撤回了 revert `refactor(patch): make patch as an override advice` · twlz0ne/psearch.el@ec27edb · GitHub 因为 advice 无法用于 generic 方法(其实我也没认真研究过,如果你知道请告诉我),算是为了一致性作出的妥协。