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

时间:2018-06-08 08:06:40

标签: macros lisp common-lisp evaluation 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)))))

错误:

; in: DEFUN ADD
;     (X Y)
; 
; caught STYLE-WARNING:
;   undefined function: X
; 
; compilation unit finished
;   Undefined function:
;     X

1 个答案:

答案 0 :(得分:6)

(defmacro defun/memo (name args &rest body)

您通常使用&body body声明正文,而不是&rest body

变量捕获

  `(let ((memo (make-hash-table :test 'equalp)))

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

(let ((var-expr (gensym)))
  `(let ((,var-expr ,expr))
     (+ ,var-expr ,var-expr)))

循环

       (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)))))

以下应该做什么?

(loop for x in ,args collect x)

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

(loop for x in (a b c) collect x)

如您所见,代码现在尝试使用参数ab调用函数c

如果您在宏中引用args该怎么办?

(loop for x in ',args collect x)

然后,您将获得:

(loop for x in '(a b c) collect x)

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

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

(list ,@args)

哪个会扩展为:

(list a b c)

在这里,你有所有的价值观,在列表中。

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

(defun foo (&rest args) 
  ;; args is bound to a list of values
)

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

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 ...) ...)))

此外,如果您的缓存函数返回多个值,您可能希望使用multiple-value-list抓取所有值并使用values-list返回它们。

SETF

顺便说一句,代码如下:

(let ((result expr))
  (setf place result)
  result)

......没有理由不写为:

(setf place expr)

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