何时使用defparameter而不是setf?

4

我正在阅读《LISP之国》这本书,目前在第一章节中。其中有一个小程序,计算机会猜测1到100之间的数字。其代码如下:

(defparameter *small* 1)
(defparameter *big* 100)

(defun guess-my-number ()
    (ash (+ *small* *big*) -1))

(defun smaller ()
    (setf *big* (1- (guess-my-number)))
    (guess-my-number))

(defun bigger ()
    (setf *small* (1+ (guess-my-number)))
    (guess-my-number))

(defun start-over ()
    (defparameter *small* 1)
    (defparameter *big* 100)
    (guess-my-number))

到目前为止,我理解发生了什么,并且在LISP中使用'ash'执行二进制搜索对此帮助很大。然而,还有一件事情让我困惑:据我所学,您使用setf为变量分配值,使用defparameter来最初定义变量。我也理解了defparameterdefvar之间的区别(至少我认为我理解了;-))。

那么现在我的问题是:如果我应该使用setf为已初始化的变量分配值,那么为什么start-over函数使用defparameter而不是setf?这是有特殊原因呢,还是只是粗心大意?

4个回答

4

这个函数只是:

(defun start-over ()
  (setf *small* 1)
  (setf *big* 100)
  (guess-my-number))

它已经被声明为一个特殊的全局变量,不需要在函数内部一遍又一遍地这样做。

可以在函数内使用DEFPARAMETER,但这是一种不好的风格。

DEFPARAMETER用于声明全局特殊变量以及对它们进行可选的文档说明。只需一次。如果您需要多次执行此操作,则通常在重新加载整个文件或系统时执行。文件编译器也会将其识别为动态绑定变量的special声明,位于顶层位置。

示例:

文件1:

(defparameter *foo* 10)

(defun foo ()
  (let ((*foo* ...))
    ...))

文件2:

(defun foo-start ()
  (defparameter *foo* 10))

(defun foo ()
  (let ((*foo* ...))
    ...))

如果用compile-file编译File 1,Lisp 将会识别 defparameter ,并且在接下来的let中使用动态绑定。
如果用compile-file编译File 2,Lisp 将不会识别 defparameter ,并且在接下来的let中使用词法绑定。如果我们再次从这个状态编译它,将使用动态绑定。
因此,在这里,版本1更好,因为它更容易控制和理解。
在你的示例中,DEFPARAMETER 出现了多次,这是没有用的。我可能会问,“变量定义在哪里”,答案会指向多个源位置……
所以,请确保你的程序元素主要只被定义一次——除非你有一个很好的理由不这样做。

3

您有全局变量。这些可以由defconstant(用于非常不变的东西),defparameter(可以更改的常量)和defvar(不会在load时覆盖的变量)定义。

您使用setf来修改词法和全局变量的状态。由于代码没有真正定义它而是更改它,因此start-over可以使用setf。如果您将defparameter替换为defvar,则start-over将停止工作。

(defparameter *par* 5)
(setf *par* 6)
(defparameter *par* 5)
*par* ; ==> 5


(defvar *var* 5)
(setf *var* 6)
(defvar *var* 5)
*var* ; ==> 6

defconstantdefparameter类似,但一旦定义,CL实现就可以在代码中自由地将其内联。因此,如果重新定义常量,则需要重新定义所有使用它的函数,否则可能会使用旧值。

(defconstant +c+ 5)
(defun test (x)
  (+ x +c+))
(test 1)             ; ==> 6
(defconstant +c+ 6)
(test 1)             ; ==> 6 or 7
(defun test (x)
  (+ x +c+))
(test 1)             ; ==> 7

1
标准并不要求实现会内联 defconstant。它只要求变量的值永远不会改变(常量变量)。 - schaueho
1
@schaueho 重新表述。一些实现可能会将defconstant视为defparameter,并且仍然符合标准,因为程序员被禁止重新定义。 - Sylwester

2

通常,人们使用defvar来初始化全局变量。 defvardefparameter之间的区别微妙,参见CLHS中的部分,这在这里起到了作用:与defvar相比,defparameter会重新赋值,而defvar会保留旧的绑定。

关于使用什么:通常,defvar和相关的命令是作为顶层表单使用的,而不是在某个函数内部(闭包是defun上下文中最显着的例外情况)。我会使用setf,而不是defparameter


LoL的作者为什么在这些函数内部使用了defparameter呢? - Joshua Taylor
这个 start-over 函数执行某种重置操作。它的目的是消除在此期间可能发生的任何更改。通常使用 defparameter 而不是 defvar 来指示该变量可以但不应该被设置。因此,对我来说,代码试图传达在此处将 *small* 设置为 1 不是正常操作,而是重置。普通的 setf 没有同样的绊倒效果。我必须承认我不觉得这种 defparameter 的用法特别吸引人(但是我也不喜欢这种重置函数,FWIW)。 - schaueho
1
是的,我也不认为这样使用是很好的。如果程序像这样使用参数变量,使用setf进行设置是没有问题的,如果需要的话。我只是想听到“这不是惯用法”,我猜。 :) - Joshua Taylor

0

对于初学者来说,符号、变量等可能会有些令人惊讶。符号具有出乎意料的功能。举几个例子,您可以询问符号的符号值、符号包、符号名称、符号函数等。此外,符号可以声明关于它们的各种信息,例如类型,这提供了编译器可以利用以创建更好代码的建议。所有符号都是如此,例如,用于乘法的符号 * 具有执行乘法运算的符号函数。它还具有符号值,即在当前 REPL 中返回的最后一个值。

关于符号的一个关键声明性信息是它们是否是“特殊”的(是的,这是一个愚蠢的名字)。出于良好的原因,将所有全局符号声明为特殊符号是一个好习惯。 defvardefparameterdefconstant 会为您完成这项工作。我们有一个约定,所有特殊变量都以 * 开头和结尾拼写,例如 *standard-output*。这个约定非常普遍,以至于一些编译器如果您忽略了它,就会发出警告。

声明一个符号为特殊的好处之一是,当您在函数中拼写变量时,它将抑制警告。例如,`(defun faster (how-much) (incf *speed* hw-much))`会生成有关`hw-much`的警告。
一个被声明为特殊的符号最酷的功能是,它是由我们称之为动态作用域管理的,与词法作用域相对。还记得*在REPL中具有上次结果的值吗?在某些实现中,您可以有多个REPL(每个在自己的线程中运行),每个都想要有自己的*、自己的*standard-output*等等。这在Common Lisp中很容易实现;线程建立了一个“动态范围”,并绑定了应该局限于该REPL的特殊变量。
所以是的,您可以在您的示例中只是`setf *small*`;但如果您从未声明它为特殊的,则编译器会发出有关您拼写错误的警告。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接