【问题标题】:How to prevent form evaluation in lisp macros?如何防止 lisp 宏中的表单评估?
【发布时间】:2018-06-08 08:06:39
【问题描述】:

我正在尝试创建一个简单的备忘录 defun。如何防止在此代码中评估 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

【问题讨论】:

  • 剩下的代码在哪里?
  • loop 的目的是什么?

标签: macros lisp common-lisp evaluation quoting


【解决方案1】:
(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)

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

如果您在宏中引用 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
)

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

格萨什

另外,(if (gethash ...) (gethash ...) other) 可以写成(or (gethash ...) other)。这样做的好处是只需评估一次对gethash 的调用。

更重要的是(感谢@Sylwester),因为您正在编写一个通用宏,所以您无法提前知道nil 是否会是一个可能的返回值。考虑到 if/or 的编写方式,具有 nil 值将使每次都重新计算结果。您需要使用来自gethash 的辅助返回值来检查该元素是否存在:

(multiple-value-bind (value exists-p) (gethash ...)
  (if exists-p
      value
      (setf (gethash ...) ...)))

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

设置

顺便说一下,下面的代码:

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

...没有理由不写成:

(setf place expr)

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

【讨论】:

  • (or (gethash ...) (setf (gethash ...) (apply original args))) 的唯一问题是当nil 是缓存的答案时,它每次都会重新生成nil
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-30
  • 2018-09-06
  • 2020-03-09
  • 2012-01-09
  • 1970-01-01
相关资源
最近更新 更多