setf 形式 ¶本节介绍如何定义可供 setf 操作的新形式。
该宏可让你轻松为简单场景定义 setf 方法。name 为函数、宏或特殊形式的名称。
只要 name 存在与其直接对应的、用于更新它的 setter(赋值函数),
你就可以使用该宏,例如:(gv-define-simple-setter car setcar)。
该宏会将如下形式的调用
(setf (name args...) value)
转换为
(setter args... value)
这类 setf 调用在文档中规定需返回 value(值)。对于 car 和 setcar 这类场景,这一点不会有问题,因为 setcar 本身就会返回它所设置的值。如果你的 setter(赋值函数)不会返回 value,请为 gv-define-simple-setter 的 fix-return 参数传入非 nil 值。此时该宏会展开为等价于以下代码的形式:
(let ((temp value)) (setter args... temp) temp)
以此确保最终返回正确的结果。
该宏支持定义比前文形式更复杂的 setf 展开逻辑。例如,当不存在可直接调用的简单赋值函数时,
或者虽存在赋值函数但该函数所需参数与位置形式的参数不一致时,你可能需要使用该宏。
该宏对 (setf (name args…) value) 形式的表达式进行展开时,
会先根据 arglist(参数列表)绑定 setf 的参数形式 (value args…),
随后执行 body(代码体)。body 应返回一个完成赋值操作的 Lisp 表达式,
且最终需返回被设置的值。以下是该宏的使用示例:
(gv-define-setter caar (val x) `(setcar (car ,x) ,val))
若需要对展开过程进行更精细的控制,可使用 gv-define-expander 宏。例如,可读写的 substring 就可通过如下方式实现:
(gv-define-expander substring
(lambda (do place from &optional to)
(gv-letplace (getter setter) place
(macroexp-let2* (from to)
(funcall do `(substring ,getter ,from ,to)
(lambda (v)
(macroexp-let2* (v)
`(progn
,(funcall setter `(cl--set-substring
,getter ,from ,to ,v))
,v))))))))
gv-letplace 宏可用于定义行为类似 setf 的宏;例如,Common Lisp 中的 incf 宏就可通过如下方式实现:
(defmacro incf (place &optional n)
(gv-letplace (getter setter) place
(macroexp-let2* ((v (or n 1)))
(funcall setter `(+ ,v ,getter)))))
getter 会被绑定为一个可复制的表达式,该表达式返回 place(位置)的值。 setter 会被绑定为一个函数,该函数接收一个表达式 v,并返回一个将 place 设为 v 的新表达式。 body(代码体)应返回一个通过 getter 和 setter 操作 place 的 Emacs Lisp 表达式。
更多细节可参考源码文件 gv.el。
该函数会让字节编译器发出警告,提示广义变量 obsolete-name(过时名称)已过时。 若 current-name 是符号类型,则警告信息会提示应使用 current-name 替代 obsolete-name; 若 current-name 是字符串类型,则该字符串会直接作为警告信息。 when 参数应为字符串类型,用于说明该变量首次被标记为过时的时间(通常为版本号字符串)。
Common Lisp 注意事项: Common Lisp 定义了另一种指定函数
setf行为的方式, 即setf函数 — 这类函数的名称是列表形式(setf name),而非符号。 例如,(defun (setf foo) …)会定义一个函数,当setf作用于foo时会调用该函数。 Emacs 不支持这种方式:若对某个尚未定义对应展开逻辑的表达式使用setf,会触发编译时错误。 而在 Common Lisp 中这并不会报错,因为(setf func)形式的函数可能在后续代码中被定义。