旧版 Emacs Lisp 动态作用域陷阱的处理方法

4
在旧的Emacs中,没有对词法作用域进行支持。我想知道在那些年代人们是如何处理动态作用域的一个特定问题的。
假设Alice编写了一个命令"my-insert-stuff",它依赖于在"fp.el"中定义的"fp-repeat"函数(我们假设这是Bob编写的提供大量函数的函数式编程库),并且假设"fp-repeat"用于多次调用函数。
来自Alice的"init.el"一部分内容:
(require 'fp)

(defun my-insert-stuff ()
  (interactive)
  ;; inserts "1111111111\n2222222222\n3333333333" to current buffer
  (dolist (i (list "1" "2" "3"))
    (fp-repeat 10
               (lambda ()
                 (insert i)))
    (insert "\n")))

以下是 Bob 的 fp.el 部分内容:

(defun fp-repeat (n func)
  "Calls FUNC repeatedly, N times."
  (dotimes (i n)
    (funcall func)))

艾丽斯很快发现她的命令并没有像她预期的那样起作用。这是因为艾丽斯使用的i与鲍勃使用的i冲突了。在过去,艾丽斯和/或鲍勃可以做些什么来防止这种冲突发生呢?也许鲍勃可以更改docstring为

"Calls FUNC repeatedly, N times.
Warning: Never use i, n, func in FUNC body as nonlocal variables."
3个回答

5
艾丽斯会小心不在lambda函数主体中使用非局部变量,因为她知道lambda不会创建词法闭包。在Emacs Lisp中,这个简单的策略通常足以避免大部分动态作用域的问题,因为在没有并发的情况下,动态变量的本地let绑定与词法绑定大多是等效的。换句话说,“老年”Emacs Lisp开发人员(考虑到Emacs Lisp的动态作用域仍然存在)不会编写像这样的lambda函数。他们甚至不想编写,因为Emacs Lisp不是一种函数式语言(现在也不是),因此循环和显式迭代通常比高阶函数更受欢迎。对于您特定的示例,上述“老年”艾丽斯只需编写两个嵌套循环即可。

高阶函数mapcar一直被广泛使用(在较小程度上还有mapc和mapconcat)。CL包添加了许多非常有用的高阶函数:mapcan、mapcon、maplist、some、every、notany、notevery、reduce、remove-if、remove-if-not等。拒绝使用函数式编程将非常限制。 - to_the_crux
@to_the_crux 哦,亲爱的,请仔细阅读问题,并将我的答案放入其上下文中。我并没有拒绝函数式编程。我只是陈述了这样一个事实:很多 Emacs Lisp 代码更喜欢显式迭代而不是高阶函数。当然,Emacs 有这样的函数,就像你所观察到的那样,但是这些函数(至今?)很少与 lambda 一起使用,甚至更少与闭包一起使用,正是因为 Emacs 很长一段时间内都没有闭包。直到今天,Emacs Lisp 仍然不是一种函数式语言。最好的情况是,它是一种采用了一些 FP 技术的命令式语言。 - user355252

4
Emacs解决问题的方式是遵循非常严格的约定:编写高阶函数(例如您的fp-repeat)的Elisp程序员应该在意识到该函数可供他人使用时使用不寻常的变量名,当这种想法不奏效时,他们应该做他们日常祷告(在Emacs教堂中总是一个好主意)。

3
除了lunaryorn和Stefan提到的,对于你给出的特定示例,在传递给fp-repeat的funarg中,实际上根本不需要使用变量i。
也就是说,它无需将i用作变量。也就是说,它不需要在函数调用时或在特定环境下确定特定符号i的值。
这个函数真正需要的只是在函数定义时和函数所在位置的i的值。在这种情况下使用变量是过度设计,只需要其值即可。
因此,穿针引线的另一种方法是在函数定义中即lambda表达式中用值替换变量
 (defun my-insert-stuff ()
   (interactive)
   (dolist (i (list "1" "2" "3"))
     (fp-repeat 10 `(lambda () (insert ',i)))
     (insert "\n")))

这个功能很好,因为没有变量,所以不可能出现变量捕获的情况。

缺点是在编译时也没有函数:构造了一个LIST,其carlambda等。然后,在运行时评估该列表,将其解释为函数。

根据具体的用例,这可能是一个有用的方法。是的,这意味着你必须区分真正需要使用变量的上下文(函数所做的是使用VARIABLE i,而不只是值)。


感谢您和@Stefan的回答。我一直在想如何避免动态作用域elisp中的名称冲突,在官方文档中似乎很少有关于这个主题的信息。 - to_the_crux
1
这并不是特定于Elisp的。您可以通过搜索Lisp相关信息来了解更多信息。Common Lisp官方文档也描述得非常好。在旧时代,它被称为“funarg”问题(实际上,存在向下和向上的funarg问题),因此“funarg”在这里也可以是一个有用的搜索术语。 - Drew

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