如何使Association List的值为函数执行结果而非表达式?
问题场景
配置Org Agenda时,想要创建关联列表(Association List),让列表中的值是函数执行的结果,但实际得到的却是函数表达式本身,必须用eval包裹才能生效。具体需求是:所有机器的org-agenda-files都包含默认目录,特定机器可以额外添加专属项目目录。
用户编写的代码如下:
;; default-agenda-files are shared by all the environments (setq default-agenda-files '("~/Documents/Org" "~/Downloads/Org")) ;; in addition to default, register project-abc dirs for nodeABC and project-xyz dirs for nodeXYZ (setq per-node-agenda-file-mappings '(("nodeABC" . (append default-agenda-files '("~/Projects/project-abc/doc/" "~/Projects/project-abc/notes"))) ("nodeXYZ" . (append default-agenda-files '("~/Projects/project-xyz/doc" "~/Projects/project-xyz/notes")))))
执行(alist-get "nodeABC" per-node-agenda-file-mappings nil nil 'string-equal)后,得到的是未求值的表达式:
(append default-agenda-files '("~/Projects/project-abc/doc/" "~/Projects/project-abc/notes"))
而非预期的合并后列表:
("~/Documents/Org" "~/Downloads/Org" "~/Projects/project-abc/doc/" "~/Projects/project-abc/notes")
用eval包裹可以解决问题,但需要理解背后的原因。
核心原因:引用(Quoting)机制的影响
问题的根源在于你用了'(单引号)来创建整个关联列表——在Emacs Lisp中,单引号会让整个列表进入引用状态,此时Emacs不会对列表内部的任何表达式进行求值,只会把它们当作字面数据保存下来。
为什么普通列表赋值时会自动求值?
比如你写(setq my-list (append default-agenda-files '("a")))时,没有用单引号包裹(append ...),Emacs Lisp会优先执行append函数调用,把求值后的结果赋值给my-list。而创建关联列表时,'(...)把整个alist都引用了,里面的(append ...)自然也不会被执行,只是作为一个列表字面量存在。
关联列表与哈希表的共性
哈希表如果是用字面量形式创建,同样会遇到这个问题——字面量定义里的函数表达式不会被自动求值,因为引用机制阻止了求值过程。
让关联列表在赋值时完成求值的方法
有两种常用方式可以实现需求,避免后续手动eval:
方法1:用list和cons构建列表
放弃单引号包裹,改用list函数构建整个关联列表,用cons创建每个键值对,这样里面的append调用会被正常求值:
(setq per-node-agenda-file-mappings (list (cons "nodeABC" (append default-agenda-files '("~/Projects/project-abc/doc/" "~/Projects/project-abc/notes"))) (cons "nodeXYZ" (append default-agenda-files '("~/Projects/project-xyz/doc" "~/Projects/project-xyz/notes")))))
方法2:反引号+逗号选择性求值
用反引号(`)包裹整个alist,对需要求值的表达式前加逗号(,),这样只有带逗号的部分会被求值,其余部分保持字面量:
(setq per-node-agenda-file-mappings `(("nodeABC" . ,(append default-agenda-files '("~/Projects/project-abc/doc/" "~/Projects/project-abc/notes"))) ("nodeXYZ" . ,(append default-agenda-files '("~/Projects/project-xyz/doc" "~/Projects/project-xyz/notes")))))
这两种方式都会在setq赋值时就执行append,把合并后的列表作为关联列表的值保存,后续直接用alist-get就能拿到预期结果,无需再用eval。
内容的提问来源于stack exchange,提问作者ansa

