如何防止lisp宏中的表单评估?

Nex*_*Nex 3 lisp macros evaluation common-lisp quoting

我正在尝试创建一个简单的备忘录.如何防止在此代码中评估args表单?

(defmacro defun/memo (name args &rest body)
  `(let ((memo (make-hash-table :test 'equalp)))
     (defun ,name ,args
       (if (gethash (loop for x in ,args collect x) memo)
           (gethash (loop for x in ,args collect x) memo)
           (let ((result (progn ,@body)))
             (setf (gethash (loop for x in ,args collect x) memo) result)
             result)))))
Run Code Online (Sandbox Code Playgroud)

错误:

; in: DEFUN ADD
;     (X Y)
; 
; caught STYLE-WARNING:
;   undefined function: X
; 
; compilation unit finished
;   Undefined function:
;     X
Run Code Online (Sandbox Code Playgroud)

cor*_*ump 6

(defmacro defun/memo (name args &rest body)
Run Code Online (Sandbox Code Playgroud)

你通常用&body body,而不是声明身体&rest body.

变量捕获

  `(let ((memo (make-hash-table :test 'equalp)))
Run Code Online (Sandbox Code Playgroud)

memo符号是要在生成的代码来结束.如果body包含memo对例如在调用之外词法绑定的符号的引用defun/memo,则它将使用您的变量.你应该使用一个新的符号,在宏内生成gensym(在反引号之外).例如,您可以执行以下操作以避免expr两次评估:

(let ((var-expr (gensym)))
  `(let ((,var-expr ,expr))
     (+ ,var-expr ,var-expr)))
Run Code Online (Sandbox Code Playgroud)

       (if (gethash (loop for x in ,args collect x) memo)
           (gethash (loop for x in ,args collect x) memo)
           (let ((result (progn ,@body)))
             (setf (gethash (loop for x in ,args collect x) memo) result)
             result)))))
Run Code Online (Sandbox Code Playgroud)

以下应该做什么?

(loop for x in ,args collect x)
Run Code Online (Sandbox Code Playgroud)

假设您定义了一个函数(defun/memo test (a b c) ...),您将在上面注入参数的文字列表,这将导致代码包含:

(loop for x in (a b c) collect x)
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,代码现在试图调用函数a与参数bc.

如果你args在你的宏中引用了什么?

(loop for x in ',args collect x)
Run Code Online (Sandbox Code Playgroud)

然后,您将获得:

(loop for x in '(a b c) collect x)
Run Code Online (Sandbox Code Playgroud)

现在,您只是复制一个文字列表.运行上面生成的代码时,它只会构建一个新的列表(a b c).这就是你需要的吗?

你想要的是获取你的函数的所有参数,即你给出的值列表.循环可以替换为:

(list ,@args)
Run Code Online (Sandbox Code Playgroud)

哪个会扩展为:

(list a b c)
Run Code Online (Sandbox Code Playgroud)

在这里,您将列出所有值.

但Common Lisp已经提供了一种将所有参数作为列表获取的方法:

(defun foo (&rest args) 
  ;; args is bound to a list of values
)
Run Code Online (Sandbox Code Playgroud)

您生成的函数也可以这样做.

Gethash

另外,(if (gethash ...) (gethash ...) other)可以写(or (gethash ...) other).这样做的好处是gethash只能将呼叫评估一次.

更重要的是(感谢@Sylwester),因为您正在编写通用宏,所以您无法事先知道是否nil可能返回值.考虑到if /或是如何编写的,具有nil值将使结果每次重新计算.您需要使用辅助返回值gethash来检查元素是否存在:

(multiple-value-bind (value exists-p) (gethash ...)
  (if exists-p
      value
      (setf (gethash ...) ...)))
Run Code Online (Sandbox Code Playgroud)

此外,如果您的缓存函数返回多个值,您可能希望全部使用multiple-value-list它们并将其返回values-list.

SETF

顺便说一句,代码如下:

(let ((result expr))
  (setf place result)
  result)
Run Code Online (Sandbox Code Playgroud)

...没有理由不写成:

(setf place expr)
Run Code Online (Sandbox Code Playgroud)

返回值setf必须是新值.在某些情况下,它可能会导致糟糕的风格,但在这里会很好.