为什么elisp里没有alist-put

最近发现了一个lisp编程的坑:

Emacs提供了一个alist-get函数,然而,并咩有一个对应的alist-put,在自己写了一个例子之后,我发现这个函数的实现有点尴尬:为了保证对alist的引用得到同步的更新,我们只能把新元素插到链表头的后面而不是直接push到前面。因此,如果做成如下的一个函数,它做不到对nil的引用进行更新。

(defun alist-put (key alist elem)
  "Ensure (KEY . ELEM) exists in ALIST, ALIST must be non-nil."
  (if (null alist)
      (error "ALIST must be non-nil!")
    (let ((cell (assoc key alist)))
      (if (null cell)
          (let ((newcell (cons key elem))
                (head (car alist))
                (rem (cdr alist)))
            (setcdr newcell rem)
            (setcdr head newcell))
        (setcdr cell elem)))))

因此只能做成一个宏,这样接口类型就不统一了。

(defmacro alist-put-1 (key alist elem)
  "Ensure (KEY . ELEM) exists in ALIST."
  `(if (null ,alist)
       (set ,alist (list (cons ,key ,elem)))
     (let ((cell (assoc ,key ,alist)))
       (if (null cell)
           (push (cons ,key ,elem) ,alist)
         (setcdr cell ,elem)))))

我记得某个教程里说过,alist应该是“如果你想不清楚为什么在这里用就别用的”的那一类东西。如果我们需要用alist去储存配置,应该限制对它的访问方式。比如放在一个slot里面,data frame外面还套了一层,这样就不会导致出现更新引用的问题。把alist直接绑定到某个全局符号下面是坏的实践。

虽然可能不是很相关,不过最近确实看到了类似的东西:

24 年 12 月:bug#75170: add-to-alist: new function

25 年 01 月:bug#75170: add-to-alist: new function

看来也是老生常谈了,维护者大概也会觉得头大。

或者,用另一种方法去更聪明地区分()和null,既能规避空头的overhead,又能在合适的时机让它可引用就好了。比如说,把list做成maybe monad……不如直接再套一层frame引用完事。

1 个赞

別瞎猜了,用 setf

(setq alst '((1 . 2) (2 . 3) (4 . 6)))
((1 . 2) (2 . 3) (4 . 6))

(setf (alist-get 1 alst) 3)
3

alst
((1 . 3) (2 . 3) (4 . 6))
(pp (macroexpand '(setf (alist-get 99 alst) 3)))
(let*
    ((p
      (if
          (and nil
               (not
                (eq nil 'eq)))
          (assoc 99 alst nil)
        (assq 99 alst))))
  (progn
    (if p
        (setcdr p 3)
      (setq alst
            (cons
             (setq p
                   (cons 99 3))
             alst)))
    3))

You can use ‘alist-get’ in “place expressions”; i.e., as a generalized variable. Doing this will modify an existing association (more precisely, the first one if multiple exist), or add a new element to the beginning of ALIST, destructively modifying the list stored in ALIST.

上面 add-to-alist 的扯皮是提 bug report 的人坚持 setf 行为和其预期不一样,认为他用 symbol 当参数的 add-to-alist 实现更好,但没有 maintainer 强烈认为 Emacs 应该加这个函数,普遍认同这是个多佘的功能,两边都没法说服对方,提 bug report 的人又特別闲天天上邮件列表对线使致的。

5 个赞

恭喜你重新发明了 setf 的一个不可扩展的极小子集

3 个赞

我看到setf的时候,发现当时的脑回路,就像喉返神经,去心脏绕了一圈又回来了。

而且最离谱的是,alist-get的文档里写着,我也看了啊这

还是没有形成习慣,Emacs 24 支持 generalized variable 后(以前 setf 是 cl package 的一部分,gv.el 本身是通过 lexical scoping/closure 重新实现的),新加的 getter 都不会再另行加 setter,都统一用 setf 实现了。

3 个赞