将 "variable name" 传递给 defvar

Pass a "variable name" to defvar

我已经为此苦苦挣扎了两天,但我找不到答案。

我想要的是定义三个变量,abc,每个变量的值为0。

天真: (dolist (lbl '(a b c)) (defvar lbl 0))

没有按我的意愿行事。 abc 仍未定义,lbl 现在的值为 0。

我想我可以理解为什么这行不通了:defvar 是一个宏,而不是一个函数,因此我将 形式 lbl,不是label的当前值(依次为abc)。 我觉得。

但是在由此产生的宏展开中,lbl 最终不应该被链接(?)或评估(?)到我想要的值吗?显然不是,要么是做不到,要么是我做错了。

我想了解:

  1. 如何进行这项工作:(dolist (lbl '(a b c)) (defvar lbl 0))
  2. 幕后出了什么问题。我感觉它与符号或引用运算符的机制有关。

defvar 是一种特殊形式,可确保其第一个参数的符号是​​绑定变量。如果变量未绑定,则第二个参数的计算表达式成为绑定变量值。因此:

(defvar *x* 10) ; if *x* was not bound it's now 10
(defvar *x* 20) ; since *x* is defined nothing happens

请注意 *x* 未计算但未计算使用。为了通过使用一个变量来获得相同的功能,该变量的计算结果为您希望在全局范围内作为变量存在的符号,您需要执行以下操作:

(defvar b 10)
(dolist (lbl '(a b c)) 
  (when (not (boundp lbl))
    (setf (symbol-value lbl) 0)))

尽管如此,尚未绑定的那些都没有像 defvar 那样变得特殊,但至少你得到了相同的行为:

(list a b c) ; => (0 10 0)

也许你应该这样做:

(defvar *a* 0)
(defvar *b* 0)
(defvar *c* 0)

如果您有很多变量需要执行此操作,您可以这样做:

(defmacro defvars (lst value)
  (loop :for e :in lst
        :collect `(defvar ,e ,value) :into result
        :finally (return (cons 'progn result))))

(defparameter *w* 10)
(defvars (*q* *w* *e*) 1)
(list *q* *w* *e* ; ==> (1 10 1)

另外,给你的全局变量加上耳罩也很重要。一旦特殊,它将遵循动态绑定。例如。

(defun test ()
  (let ((*b* 15))
    (test2)))

(defun test2 ()
  *b*)

(test) ; ==> 15 

重新实现 DEFVAR

您可以使用如下函数近似 defvar 的行为:

(defun %defvar (symbol value documentation)
  "Define a global special variable.

symbol---a symbol
value---nil or a function of zero arguments
documentation---nil or a documentation string

returns symbol

Proclaim SYMBOL globally as a special variable.  If VALUE is non-nil,
then if SYMBOL is not already bound, SYMBOL is assigned the value
returned by calling VALUE.  DOCUMENATION is assigned as the
documentation of type variable to for SYMBOL."
  (prog1 symbol
    ;; make it globally special
    (proclaim (list 'special symbol))
    ;; if a value is provided, and symbol isn't
    ;; already bound, set its value to the result
    ;; of calling the value-function
    (when (not (null value))
      (unless (boundp symbol)
        (setf (symbol-value symbol)
              (funcall value))))
    ;; set the documentation
    (setf (documentation symbol 'variable) documentation)))

那么你可以这样做,例如

CL-USER> (%defvar '*the-answer* (lambda () 42) "the answer")
*THE-ANSWER*
CL-USER> *the-answer*
42
CL-USER> (documentation '*the-answer* 'variable)
"the answer"

使用您的原始代码,您可以执行以下操作:

(dolist (lbl '(a b c)) (%defvar lbl (lambda () 0)))

现在,这与 defvar 的实际作用有什么关系?那么,您现在可以通过执行以下操作来实现类似 defvar 的宏:

(defmacro define-var (symbol &optional (value nil value-p) documentation)
  `(%defvar
    ',symbol
    ,(if value-p `(lambda () ,value) 'nil)
    ,documentation))

如我们所料展开:

CL-USER> (macroexpand-1 '(define-var *the-answer* 42 "the answer"))
(%DEFVAR '*THE-ANSWER* (LAMBDA () 42) "the answer")

您实际上也可以使用 macroexpand 来查看实现的功能。例如,在 SBCL 中:

CL-USER> (macroexpand-1 '(defvar *the-answer* 42 "the answer"))
(PROGN
 (EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-IMPL::%COMPILER-DEFVAR '*THE-ANSWER*))
 (SB-IMPL::%DEFVAR '*THE-ANSWER* (UNLESS (BOUNDP '*THE-ANSWER*) 42) 'T
                   "the answer" 'T (SB-C:SOURCE-LOCATION)))

这与我们上面写的没有太大区别,尽管它处理的是当变量已经以稍微不同的方式绑定时对表单的非评估,并且它还有一些用于记录源的处理地点。不过,总体思路是一样的。

为什么事情没有得到 "linked up"

But in the resulting macroexpansion, shouldn't lbl eventually be linked-up(?) or evaluated(?) to the value I'm intending?

原代码为:

(dolist (lbl '(a b c)) (defvar lbl 0))

我们可以对其进行宏扩展,看看它会变成什么(在 SBCL 中):

CL-USER> (macroexpand '(dolist (lbl '(a b c)) (defvar lbl 0)))
(BLOCK NIL
  (LET ((#:N-LIST1022 '(A B C)))
    (TAGBODY
     #:START1023
      (UNLESS (ENDP #:N-LIST1022)
        (LET ((LBL (TRULY-THE (MEMBER C B A) (CAR #:N-LIST1022))))
          (SETQ #:N-LIST1022 (CDR #:N-LIST1022))
          (TAGBODY (DEFVAR LBL 0)))
        (GO #:START1023))))
  NIL)
T

现在,我们仍然可以在两个地方看到 LBL,包括 (defvar LBL 0)。那么为什么事情没有得到 "matched up"?要看到这一点,我们需要记住 defvar inside let 也将被宏扩展。什么?这个:

CL-USER> (macroexpand '(DEFVAR LBL 0))
(PROGN
 (EVAL-WHEN (:COMPILE-TOPLEVEL) (SB-IMPL::%COMPILER-DEFVAR 'LBL))
 (SB-IMPL::%DEFVAR 'LBL (UNLESS (BOUNDP 'LBL) 0) 'T NIL 'NIL
                   (SB-C:SOURCE-LOCATION)))

但现在我们看到 SBCL 的内部正在获取名为 "LBL" 的符号;调用 (sb-impl::%defvar 'lbl …) 使用 [=49 调用函数 sb-impl::%defvar =]symbol lbl,并且该符号与恰好在源代码中由相同符号表示的词法变量之间没有任何联系。毕竟,如果你写:

CL-USER> (let ((a 89))
           (list 'a a))
(A 89)

希望能够得到符号a和数字89,正确的? defvar 的宏扩展包括调用带有宏参数之一的 quotation 的函数。

这里有几个选项:

使用 eval,通过构建 defvar 表达式:

(dolist (lbl '(a b c))
  (eval `(defvar ,lbl 0))

proclaim and setf of symbol-value (note: set is deprecated,自 1994 年以来的价值):

(dolist (lbl '(a b c))
  (proclaim `(special ,lbl))
  (setf (symbol-value lbl) 0))

这实际上主要是 defvar 所做的(请参阅链接页面中的注释),但每个 Lisp 实现通常也记录源文件位置,就像它们对其他定义宏所做的那样。


在幕后,defvar 是一个使变量 special (i.e. with dynamic extent bindings in the current dynamic environment 的宏;注意:没有可移植的撤消操作!),如果尚未绑定,则可以选择对其进行初始化。

它是一个宏这一事实意味着它不会评估其参数,因此它可以按字面意思使用变量名,而且它确实这样做了。因此,(defvar lbl 0) 将定义变量 lbl 而不是 存储在 lbl 变量中的符号。

它可选地初始化变量的事实意味着如果变量是 boundp,甚至不会评估初始化表达式。因此,如果变量已经初始化,则不会发生其副作用。这可能是预期的,也可能不是预期的。

请注意,这个表达式实际上并没有在宏展开时求值,它留待展开时求值,这在 REPL 中意味着在宏展开之后立即执行(也可能在编译之后,具体取决于 Lisp 实现; 阅读更多关于 evaluation and compilation 的内容,这很有趣)。

相似:

(dolist (lbl '(a b c))
  (let ((lbl 0))
    (print lbl)))

为什么是 lbl 0 而不是 a, b, c?

因为 LET 绑定了符号 lbl 而不是它的值。

类似于(DEFVAR FOO 3)

想象以下代码:

(DEFVAR FOO 3)
(LET ((FOO 3)) ...)

现在,如果我们编译这段代码,Lisp 编译器会识别 DEFVAR 声明并且现在知道 FOO 是一个特殊的全局变量。因此在 let 形式中 FOO 将被动态绑定。

比较这段代码:

(dolist (v '(FOO)) (eval `(DEFVAR ,v 3)))
(LET ((FOO 3)) ...)

编译器不会看到 DEFVAR 并且不知道它应该是一个全局特殊变量。在 LET 形式中,FOO 将具有词法绑定。

因此 DEFVAR 需要是一个宏,它在编译时 (!) 知道该符号,并扩展为一种形式,通知编译器该符号是一个特殊的全局变量。表单也会在执行时设置值。

因此,从变量列表创建多个 DEFVAR 声明的最佳方法是编写一个宏,它扩展为具有多个 DEFVARPROGN 形式。在 PROGN 内,编译器仍会识别它们。

CL-USER 21 > (pprint (macroexpand '(defvar* (a b c) 0)))

(PROGN (DEFVAR A 0) (DEFVAR B 0) (DEFVAR C 0))

实施为:

(defmacro defvar* (vars initial-value)
  `(progn
     ,@(loop for var in vars
             do (check-type var symbol)
             collect `(defvar ,var ,initial-value))))

请注意,检查变量是否确实作为符号提供是有意义的。