如何在Emacs Lisp动态作用域下编程?

58

我之前学习过Clojure并且非常喜欢这门语言。我也很喜欢Emacs,并用Emacs Lisp编写了一些简单的东西。然而,有一件事情会让我在使用Elisp时产生心理障碍,那就是动态作用域的概念。它对我来说太陌生了,感觉像是半全局变量。

因此,在变量声明方面,我不知道哪些操作是安全的,哪些是危险的。据我所知,使用setq设置的变量属于动态作用域(是这样吗?)那么let变量呢?我在某个地方读到let允许你使用纯词法作用域,但在其他地方又读到let变量也是动态作用域。

我想我的最大担心是我使用setq或let时,我的代码可能会意外破坏平台或第三方代码中的一些变量,或者在调用后我的本地变量会被意外地破坏。我该如何避免这种情况?

有没有一些简单的经验法则可以遵循,并且不用担心以某种奇怪和难以调试的方式被咬住?

10个回答

48

情况并不那么糟糕。

主要问题可能出现在函数中的“自由变量”。

(defun foo (a)
  (* a b))
在上面的函数中,a是一个局部变量,b是一个自由变量。在具有动态绑定的系统(比如Emacs Lisp)中,将在运行时查找b。现在有三种情况:
  1. b未定义 -> 出错
  2. b是当前动态作用域中某个函数调用绑定的局部变量 -> 取该值
  3. b是全局变量 -> 取该值
问题可能是:
  • 被函数调用遮蔽的已绑定值(全局或局部)可能是不想要的
  • 未定义的变量未被遮蔽 -> 访问时出错
  • 全局变量未被遮蔽 -> 获取全局值,这可能是不想要的
在编译器中编译上述函数可能会生成警告,说明存在自由变量。通常Common Lisp编译器会这样做。解释器不会提供该警告,只能在运行时看到效果。 建议
  • 确保不会意外使用自由变量
  • 确保全局变量有一个特殊的名称,以便在源代码中易于识别,通常为*foo-var*
不要写:
(defun foo (a b)
   ...
   (setq c (* a b))  ; where c is a free variable
   ...)

编写:

(defun foo (a b)
   ...
   (let ((c (* a b)))
     ...)
   ...)

绑定所有您想要使用的变量,并确保它们在其他地方没有被绑定。

基本上就是这样。

自GNU Emacs版本24开始,其Emacs Lisp支持词法绑定。参见:词法绑定,GNU Emacs Lisp参考手册


谢谢!看起来你的建议可以防止我在调用堆栈中意外搞砸东西。但我还有点担心会在调用堆栈下方引起麻烦... - auramo
3
如果你遵循这个建议,你的代码也应该在调用栈下正常工作。 - Rainer Joswig

14

除了Gilles回答中的最后一段之外,这里是RMS如何支持可扩展系统中的动态作用域的论述:

一些语言设计者认为应该避免动态绑定,而应该使用显式参数传递。想象一下函数A绑定变量FOO并调用函数B,B又调用函数C,C使用变量FOO的值。据说A应将该值作为参数传递给B,B再将其作为参数传递给C。然而,在可扩展系统中无法这样做,因为系统的作者无法知道所有参数都是什么。想象一下函数A和C是用户扩展的一部分,而B是标准系统的一部分。变量FOO不存在于标准系统中,它是扩展的一部分。使用显式参数传递需要向B添加一个新参数,这意味着重写B和调用B的所有内容。在最常见的情况下,B是编辑器命令分派循环,从许多地方调用。更糟糕的是,C还必须传递一个额外的参数。B没有以名称引用C(B编写时C不存在)。它可能在命令分派表中找到指向C的指针。这意味着有时调用C的相同调用也可能调用任何编辑器命令定义。因此,所有编辑命令都必须重写以接受和忽略附加参数。现在,原始系统已经不存在了!

个人认为,如果Emacs-Lisp存在问题,那不是动态作用域本身的问题,而是它是默认设置,并且不能在不使用扩展的情况下实现词法作用域。在CL中,可以使用动态和词法作用域,并且-除了顶层(由几个deflex实现解决)和全局声明的特殊变量之外-默认为词法作用域。在Clojure中,您也可以使用词法和动态作用域。

再次引用RMS的话:

提供动态作用域规则并不是必需的,但有用的是它可用。


3
通常解决“可扩展性问题”的方法是使用“对象”,而不是将字段添加到参数列表中,对象可以获得额外的字段。例如,不要将所有绘图参数都传递给函数,而是传递一个绘图上下文对象。添加新的绘图参数就是将其添加到绘图上下文类的插槽中。但由于Emacs Lisp没有对象系统,这种解决方案也不可用。即使RMS对Lisp的'Flavors'对象系统很熟悉。 - Rainer Joswig

13

有没有一些简单的经验法则,可以让我遵循并知道作用域会发生什么,而不会被某些奇怪、难以调试的方式所困扰?

阅读Emacs Lisp Reference,你将了解到像这样的许多细节:

  • 特殊形式:setq [symbol form]... 这个特殊形式是改变变量值最常见的方法。每个符号都被赋予一个新值,该值是评估相应表达式的结果。更改的是符号的最局部现有绑定

以下是一个示例:

(defun foo () (setq tata "foo"))

(defun bar (tata) (setq tata "bar"))


(foo)
(message tata)
    ===> "foo"


(bar tata)
(message tata)
    ===> "foo"

谢谢,你提供的例子和参考代码片段让我更好地理解了这个问题。我之前读过一些参考资料,但是似乎从未完全掌握这个问题... - auramo

13

正如Peter Ajtai指出的:

自emacs-24.1以来,您可以通过将

lexical-binding: t

添加到文件顶部来在每个文件上启用词法作用域。

;; -*- lexical-binding: t -*-

在您的Elisp文件顶部。


10

首先,Elisp具有独立的变量和函数绑定,因此某些动态作用域的陷阱并不相关。

其次,您仍然可以使用setq来设置变量,但是所设置的值不会在动态范围退出后保留。从根本上讲,这与词法作用域没有区别,不同之处在于动态作用域中调用的函数中的setq可能会影响函数调用后看到的值。

如果绝对需要的话,有一个名为lexical-let的宏,它(基本上)模仿了词法绑定(我相信它通过遍历主体并将所有词法let变量的出现更改为gensymmed名称来实现这一点,最终取消注册符号)。

我认为“按照正常方式编写代码”。在实践中,Elisp的动态特性会咬你的时候很少,但也有这样的时候。

下面是一个关于setq和动态绑定变量的示例(最近在附近的scratch缓冲区中评估):

(let ((a nil))
  (list (let ((a nil))
          (setq a 'value)
          a)
        a))

(value nil)

嗯,对我来说,setq似乎在本质上与词法作用域非常不同...几乎是全局的。我想我的最大担忧是,我的代码(使用setq或let)会意外地破坏我调用的平台或第三方代码中的某些变量,或者在调用后我的变量会被意外地搞乱。我该如何避免这种情况?通过命名约定吗?这甚至是一个相关的担忧吗? - auramo
1
在elisp中,let建立一个新的动态绑定,对该变量所做的任何操作都会在动态作用域结束时停止(即执行超出let块时)。函数参数也是一样的(建立一个新的绑定)。 - Vatine
1
另外,我的示例展示了两个动态作用域,其中一个在另一个内部,内部的作用域修改了动态绑定变量,而“外部”的绑定没有受到影响。 - Vatine

8

这里写的所有内容都是值得一看的。我想再补充一点:了解一下Common Lisp——即使仅仅是阅读相关资料也好。CLTL2以及其他书籍很好地介绍了词法和动态绑定。而且,Common Lisp将它们整合到一个语言中的能力非常出色。

如果你对Common Lisp有所了解,那么理解Emacs Lisp的时候就会更加清晰明了。虽然Emacs 24默认使用的是词法作用域,但是在我的看法中,Common Lisp的方法更加清晰、简洁。最后,RMS和其他人强调过,动态作用域对于Emacs Lisp来说是非常重要的。

因此,我的建议是先了解一下Common Lisp是如何处理这些问题的。如果你主要的Lisp思维模式是Scheme,那么请尝试忘记它——这会限制你更多,而不是帮助你理解Emacs Lisp中的作用域、函数参数等等。Emacs Lisp和Common Lisp一样“肮脏低级”,它并不是Scheme。


2
Emacs 24.1 刚刚发布 - 您可以使用词法作用域来处理本地变量,eval 具有词法选项,并且具有新形式的词法作用域解释函数 - http://www.masteringemacs.org/articles/2011/12/12/what-is-new-in-emacs-24-part-2/ - 在“Emacs 24.1 中的 Lisp 更改”下。 - Peter Ajtai
Emacs Lisp中的词法作用域仍然有限。我建议看看Common Lisp。它应该(但永远不会完全)成为Emacs Lisp的模型,以在支持词法和动态绑定方面进行融合。 - Drew

6

动态作用域和词法作用域在代码被用于不同于定义它的作用域时有不同的行为。实际上,有两种模式可以覆盖大多数麻烦的情况:

  • A function shadows a global variable, then calls another function that uses that global variable.

    (defvar x 3)
    (defun foo ()
      x)
    (defun bar (x)
      (+ (foo) x))
    (bar 0) ⇒ 0
    

    This doesn't come up often in Emacs because local variables tend to have short names (often single-word) whereas global variables tend to have long names (often prefixed by packagename-). Many standard functions have names that are tempting to use as local variables like list and point, but functions and variables live in separate name spaces are local functions are not used very often.

  • A function is defined in one lexical context and used outside this lexical context because it's passed to a higher-order function.

    (let ((cl-y 10))
      (mapcar* (lambda (elt) (* cl-y elt)) '(1 2 3)))
    ⇒ (10 20 30)
    (let ((cl-x 10))
      (mapcar* (lambda (elt) (* cl-x elt)) '(1 2 3)))
    ⇑ (wrong-type-argument number-or-marker-p (1 2 3))
    

    The error is due to the use of cl-x as a variable name in mapcar* (from the cl package). Note that the cl package uses cl- as a prefix even for its local variables in higher-order functions. This works reasonably well in practice, as long as you take care not to use the same variable as a global name and as a local name, and you don't need to write a recursive higher-order function.

P.S. Emacs Lisp 之所以采用动态作用域,不仅仅是因为它的年龄。确实,在当时,Lisp 倾向于使用动态作用域——Scheme 和 Common Lisp 还没有真正流行起来。但是,动态作用域在面向动态系统扩展的语言中也是一种优势:它可以让你轻松地钩入更多的位置,而无需特殊的努力。伴随着强大的能力而来的是悬崖峭壁:你可能会意外地钩入一个你不知道的地方。


谢谢,你提到了命名规范,而其他人都跳过了。太棒了! - auramo
1
我同意你所说的一切,除了"还没有真正开始"这句话。当RMS编写GNU Emacs Lisp时,Common Lisp和Scheme已经存在了一段时间。Emacs比两者都要早,但基于Lisp的Emacs不是。Emacs Lisp的设计和创建是在Common Lisp和Scheme之后进行的。 - Drew

4
其他答案在解释如何使用动态作用域方面技术上较好,所以这是我的非技术建议:
尽管去做。
我已经 tinkering with Emacs lisp for 15+ years, 由于词法/动态范围之间的差异而导致的任何问题都不知道是否存在。
个人而言,我并没有发现需要 closures(我喜欢他们,只是不需要他们用于 Emacs)。 并且,我通常尽量避免全局变量(无论作用域是词法还是动态)。
因此,我建议您开始编写符合您需求和期望的自定义内容,很可能您不会遇到任何问题。

2

不要这样做。

Emacs-24 允许您使用词法作用域。只需运行

(setq lexical-binding t)

或在文件开头添加

;; -*- lexical-binding: t -*-

即可。


2
我完全理解你的痛苦。我发现emacs缺乏词法绑定,特别是不能使用词法闭包,这似乎是我经常想到的一种解决方法,来自更现代的语言。
虽然我没有更多关于解决之前回答中未涉及的缺失功能的建议,但我想指出存在一个名为“lexbind”的emacs分支,以向后兼容的方式实现词法绑定。根据我的经验,在某些情况下词法闭包仍然有些错误,但该分支似乎是一个有前途的方法。

3
对于任何来得晚的人,lexbind 分支已经合并并在 Emacs 24 中发布。 - phils
@phils 我认为这是一个足够重要的公告,你有权对主要问题发表评论。这将有助于建立这个问题的历史背景。 - Joshua Taylor

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