问题描述
在某些时候,Emacs 添加了 pure
符号属性,指示已知哪些函数是纯函数(参见 here)。有没有办法使用这个属性来确定整个表达式是否是常量和无副作用的?例如,很容易确定 (car '(1 2 3))
是一个常量表达式,因为 car
是一个纯函数(即 (get 'car 'pure)
返回 t)而 '(1 2 3)
是一个带引号的形式。但是,还有更复杂的表达式,例如涉及特殊形式,它们仍然是不变的:
(let ((a (+ 1 2))
(b (- 5 5)))
(if (> b 0)
a
(- a)))
值得注意的是,let
和 if
都没有被标记为纯,但这仍然是一个计算为 -3 的常量表达式。
有什么办法可以让我用任意表达式来判断它是否是常量?我知道有 unsafep
,它似乎实现了必要的表达式树遍历,但它评估的标准与“常量”不同,因此我不能直接使用它。
- 可以假设标准 Elisp 函数、宏或特殊形式(
car
、let
、if
等)的任何定义都没有被修改。 立>
- 就我而言,“常量”是使用
equal
定义的。因此,在每次调用时返回具有相同内容的新列表的函数将被视为常量。 - 我知道有些常量表达式涉及不纯函数,例如
(nreverse '(1 2 3))
。我不介意算法是否遗漏了这样的表达式。
以防万一,我想这样做的原因是我正在实现一个 elisp 宏,其中出现在特定上下文中的常量表达式在语法上是有效的,但它们的返回值将被忽略,使它们毫无意义,并且可能程序员误解的结果。因此,如果宏在此上下文中看到常量表达式,我想发出警告。
解决方法
有什么办法可以让我用任意的表达式来判断它是否是常量?
没有
首先,每当 lisp 解释器看到一个符号时,如果它以前从未见过,它就会被驻留在 obarray
(对象数组)中。所以严格意义上来说,大多数函数不可能是无副作用的。
此外,根据定义,某些操作具有副作用。例如,IO 在从 stdin
读取或写入 stdout
/stderr
时会产生副作用。
因此我们需要将 side-effect-free
的定义限制为不改变外部状态。例如,以下内容不得为 side-effect-free
:
;; If a function modifies one of its input argument
(defvar foo nil)
(funcall (lambda () (setq foo t)))
;; If a function modifies a free variable. Here `free variable` means any non-constant ;; symbol from the enclosing environment
(funcall (lambda (x) (setq x t)) foo)
(funcall (lambda (x) (makunbound x)) 'foo)
;; If a function initialize a variable in the enclosing environment
(funcall (lambda (x) (defvar bar nil)))
其次,pure
函数必须是 side-effect-free
并且在给定相同输入的情况下返回相同的输出。
constant
在此上下文中并不意味着符号具有不变的值。这意味着在编译时可以知道 sexp
的值。这与 referential transparency
相关,但不相等。如果 sexp
是 constant
,字节编译器可以简单地将 sexp
替换为其值。
也就是说,(declare (pure t))
告诉编译器,如果此函数的所有输入参数都是常量(它们在编译时已知),则可以在编译时计算输出。
判断一个函数是否为side-effect-free
,只需要检查符号的符号属性side-effect-free
或pure
。或者你可以使用:
(defun side-effect-free-p (function)
(and (symbolp function)
(or (function-get function 'pure)
(funciton-get function 'side-effect-free))))
同样,
(defun pure-p (function)
(and (symbolp fucntion)
(function-get function 'pure)))
绝大多数函数既不是side-effect-free
也不是pure
,但在特定调用中它们可以是side-effect-free
或pure
。例如,
;; `let` - pure and side-effect free
(let ((foo nil))
nil)
;; `let` - neither pure nor side-effect free
(defvar bar t)
(let ((foo (setq bar nil))) ; modified outside state,bar
nil)
;; `let` - not pure but side-effect free
(let ((foo (current-time))) ; the first argument to let,not constant
foo)
(let () ; empty local binding specification
(current-time)) ; the second argument to let,it's not constant