Common Lisp:有什么办法可以避免 defvar 或 defparameter?

问题描述

我正在使用 SBCL 2.0.1.debian 和 Paul Graham 的 ANSI Common Lisp 来学习 Lisp。

尽管在第 2 章中,我意识到我不能像作者那样使用 setf!稍微谷歌搜索,我了解到我必须使用 defvardefparameter 来“引入”我的全局变量,然后才能使用 setq 设置它们!

有什么办法可以避免通过 defvardefparameter 引入全局变量,无论是从 SBCL 内部还是从外部通过开关?其他 Lisp 是否也要求这样做?

我了解它们在大型代码库中的价值,但现在我只是通过编写小型程序来学习,因此我发现它们很麻烦。我习惯于在其他语言中使用全局变量,所以不必介意全局/局部变量错误

解决方法

如果您正在编写程序,从某种意义上说,文件中存在一些持久存在的东西,那么请使用 def* 形式。未定义变量的顶级 setf / setq 在 CL 中具有未定义的语义,更糟糕的是,在不同的实现中具有不同的语义。键入 defvardefparameterdefconstant 并不比键入 setfsetq 难多少,这意味着您的程序将具有定义的语义,这通常被认为是一种好的事物。所以不,对于程序,没有办法避免使用 def* 形式,或某种等效形式。

如果你只是在 REPL/监听器上输入东西来玩东西,那么我认为只在顶级使用 setf 就可以了(没有人使用在 REPL 输入的东西真的持久的环境我想多了)。

您说您习惯于在其他语言中使用全局变量。取决于那些其他语言是什么,这很可能意味着您习惯于使用 def* 形式定义的绑定的 CL 语义,这不仅是全局的,而且全局特殊,或全局动态。我不知道哪些其他语言甚至具有 CL 的特殊/词汇区别,但我怀疑没有那么多。例如考虑这个 Python (3) 程序:

x = 1

def foo():
    x = 0
    print(x)
    def bar():
        nonlocal x
        x += 1
        return x
    return bar

y = foo()

print(y())
print(y())
print(x)

如果你运行它会打印

0
1
2
1

所以考虑“等效”的 CL 程序(注意:永远不要用这样命名的变量编写程序):

(defvar x 1)

(defun foo ()
  (let ((x 0))
    (print x)
    (lambda ()
      (incf x))))

(let ((y (foo)))
  (print (funcall y))
  (print (funcall y))
  (print x)
  (values))

如果你运行它会打印

0
2
3
3

我认为您可以同意这与 Python 程序打印的内容大不相同。

那是因为 CL 中的顶级变量是特殊的,或动态变量:在上面的 CL 程序中,x 是动态范围的,因为 x 已被 defvar 声明为全局特殊的,这意味着对 foo 返回的函数的调用正在改变 x 的全局值。

Python 根本没有动态绑定(但您可以编写 Python 模块,将它们模拟为访问显式绑定堆栈的函数,但有点狡猾)。类似的事情也是其他语言公开动态绑定的方式。例如,Racket 就是这样做的:

(define d (make-parameter 1))

(define (foo)
  (parameterize ([d 0])
    (display (d))
    (λ ()
      (d (+ (d) 1))
      (d))))

(let ([y (foo)])
  (display (y))
  (display (y))
  (display (d)))

将打印

0
2
3
3

虽然这个程序

(define x 1)

(define (foo)
  (let ([x 0])
    (displayln x)
    (λ ()
      (set! x (+ x 1))
      x)))

(let ([y (foo)])
  (displayln (y))
  (displayln (y))
  (displayln x))

将打印

0
1
2
1

就像 Python 那样。

如果不是 CL 的兼容性要求,这也许也是 CL 应该这样做的方式(这显然是个人意见,我对 CL 的方式感到满意)。

CL 根本没有顶级(全局)词法 绑定(但您可以编写一个 CL 程序,该程序将使用符号宏以非常令人信服的方式模拟它们)。如果你想要这样的东西,我怀疑互联网上已经有一些东西,或者我可以继续整理我的实现。作为一个使用这种东西的程序的例子:

(defglex x 1)

(defun foo ()
  (let ((x 0))
    (print x)
    (lambda ()
      (incf x))))

(let ((y (foo)))
  (print (funcall y))
  (print (funcall y))
  (print x))

将打印

0
1
2
1

CL 具有词法和动态(特殊)非全局绑定。

注意:使用 def* 形式定义的事物是全局特殊/动态的,这意味着它们的所有绑定都是动态的,这就是为什么您应该始终使用 * 约定来区分此类变量的名称:(defvar *my-var* ...)never (defvar my-var ...)。 (不过,(defconstant my-constant ...) 没问题。)