我如何判断 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)))
值得注意的是,let
和 if
都没有被标记为纯的,但这仍然是一个计算结果为 -3 的常量表达式。
有什么方法可以取任意表达式并确定它是否为常量?我知道有 unsafep
,它似乎实现了必要的表达式树遍历,但它正在评估与“常量”不同的标准,所以我不能直接使用它。
- 假设 none 标准 Elisp 函数、宏或特殊形式(
car
、let
、if
等)的定义是正确的已修改。
- 为了我的目的,“常量”是使用
equal
定义的。因此,一个函数 return 每次调用时都会生成一个新列表且内容相同,这将被视为常量。
- 我知道有一些常量表达式涉及不纯函数,例如
(nreverse '(1 2 3))
。我不介意算法是否遗漏了这样的表达式。
万一重要,我想这样做的原因是我正在实现一个 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
有关,但不相等。如果 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
在某些时候,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)))
值得注意的是,let
和 if
都没有被标记为纯的,但这仍然是一个计算结果为 -3 的常量表达式。
有什么方法可以取任意表达式并确定它是否为常量?我知道有 unsafep
,它似乎实现了必要的表达式树遍历,但它正在评估与“常量”不同的标准,所以我不能直接使用它。
- 假设 none 标准 Elisp 函数、宏或特殊形式(
car
、let
、if
等)的定义是正确的已修改。 - 为了我的目的,“常量”是使用
equal
定义的。因此,一个函数 return 每次调用时都会生成一个新列表且内容相同,这将被视为常量。 - 我知道有一些常量表达式涉及不纯函数,例如
(nreverse '(1 2 3))
。我不介意算法是否遗漏了这样的表达式。
万一重要,我想这样做的原因是我正在实现一个 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
有关,但不相等。如果 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