我如何判断 elisp 表达式是否纯粹且恒定?

How can I tell if an elisp expression is pure and constant?

在某些时候,Emacs 添加了 pure 符号 属性,表明哪些函数是已知的纯函数(参见 here)。有没有办法使用这个 属性 来确定整个表达式是否恒定且无副作用?例如,很容易确定 (car '(1 2 3)) 是一个常量表达式,因为 car 是一个纯函数(即 (get 'car 'pure) returns t)而 '(1 2 3) 是引用的形式。但是,还有更复杂的表达式,例如涉及特殊形式,仍然不变:

(let ((a (+ 1 2))
      (b (- 5 5)))
  (if (> b 0)
      a
    (- a)))

值得注意的是,letif 都没有被标记为纯的,但这仍然是一个计算结果为 -3 的常量表达式。

有什么方法可以取任意表达式并确定它是否为常量?我知道有 unsafep,它似乎实现了必要的表达式树遍历,但它正在评估与“常量”不同的标准,所以我不能直接使用它。

万一重要,我想这样做的原因是我正在实现一个 elisp 宏,其中出现在特定上下文中的常量表达式在语法上是有效的,但它们的 return 值将被忽略,使它们毫无意义,并且可能是程序员误解的结果。因此,如果宏在此上下文中看到常量表达式,我想发出警告。

Is there any way I can take an arbitrary expression and determine whether it is constant?

没有


首先,每当 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 AND returns 给定相同输入的相同输出。

constant 在此上下文中并不意味着符号具有不变的值。这意味着可以在编译时知道 sexp 的值。这与 referential transparency 有关,但不相等。如果 sexpconstant,字节编译器可以简单地用它的值替换 sexp

也就是说,(declare (pure t)) 告诉编译器,如果这个函数的所有输入参数都是常量(它们在编译时已知),则可以在编译时计算输出。


判断一个函数是否为side-effect-free,只需要查看符号的属性side-effect-freepure即可。或者您可以使用:

(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-freepure。例如,

;; `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